Zum Inhalt springen
UI Kit
Accent

KI-Einstellungen

Einstellungen eines KI-Assistenten. Ein Gedächtnis zum Ansehen und Bearbeiten, eigene Anweisungen mit Live-Vorschau, Datenkontrolle mit Export und Löschen, verbundene Apps mit Zugriff je Bereich und Regeln für einen ganzen Arbeitsbereich.

Eine von 3 Components in KI-Einrichtung

5 Variants

Variants

Gedächtnis

Jeder gemerkte Fakt mit Herkunft, gruppiert, durchsuchbar, direkt bearbeitbar und mit Rückgängig vergessbar. Ein Schalter pausiert das Gedächtnis.

Im Studio öffnen

Eigene Anweisungen

Zwei Felder mit Zähler, Tonvorlagen als Auswahl und eine Vorschau, deren Beispielantwort sich mit der Vorlage ändert. Speichern leuchtet erst nach einer Änderung auf.

Im Studio öffnen

Datenkontrolle

Training standardmäßig aus, wie lange Chats bleiben, vorübergehende Chats, ein Export und das Löschen aller Chats hinter einer getippten Bestätigung.

Im Studio öffnen

Verbundene Apps

Jede verbundene App mit ihren Bereichen als Schalter und der letzten Nutzung. Zeilen öffnen weich, Trennen ist ein Klick.

Im Studio öffnen

Regeln für das Team

Regeln für alle im Arbeitsbereich zu sensiblen Daten, Training, Protokollen und erlaubten Modellen, gemeinsam angewendet und protokolliert.

Im Studio öffnen

Details

Was KI-Einstellungen mitbringt, aus Registry und Quellcode gelesen.

Bereich
App
Variants
5
Version
1.0.0 · seit 1.1.0
Datei
components/ui/ai-settings.tsx
Bringt mit
utilskiticonrevealmotionswitch
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 61 eigene Texte

Props

Die Schnittstelle AiSettingsProps aus components/ui/ai-settings.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • variantoptional
    AiSettingsVariant

    memoryinstructionsdata-controlsconnected-appsworkspace-policy

  • titleoptional
    string
  • descriptionoptional
    string
  • memoriesoptional
    AiMemory[]

    memory

  • memoryOnoptional
    boolean
  • aboutoptional
    string

    instructions

  • styleoptional
    string
  • presetsoptional
    AiTonePreset[]
  • sampleQuestionoptional
    string

    instructions: the question the sample answer replies to

  • maxLengthoptional
    number
  • retentionoptional
    string[]

    data-controls: how long chats are kept, the first is the default

  • connectionsoptional
    AiConnection[]

    connected-apps

  • rulesoptional
    AiPolicyRule[]

    workspace-policy

  • modelsoptional
    { id: string; name: string; allowed: boolean }[]

Eingebaute Texte

Diese Texte schreibt KI-Einstellungen selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 61 Texte anzeigen
memoryOnEnglisch Remember things across chatsDeutsch Über Chats hinweg merken
memoryNoteEnglisch The assistant keeps short facts you share, so you do not have to repeat them. You see and control every one of them here.Deutsch Der Assistent merkt sich kurze Fakten, die du teilst, damit du sie nicht wiederholen musst. Jeden davon siehst und steuerst du hier.
searchEnglisch Search memoryDeutsch Gedächtnis durchsuchen
countEnglisch {n} entriesDeutsch {n} Einträge
learnedEnglisch From {source}Deutsch Aus {source}
editEnglisch Edit {name}Deutsch {name} bearbeiten
saveEnglisch SaveDeutsch Speichern
cancelEnglisch CancelDeutsch Abbrechen
forgetEnglisch Forget {name}Deutsch {name} vergessen
forgottenEnglisch Forgotten. The assistant no longer uses it.Deutsch Vergessen. Der Assistent nutzt es nicht mehr.
undoEnglisch UndoDeutsch Rückgängig
forgetAllEnglisch Forget everythingDeutsch Alles vergessen
forgetAllAskEnglisch Forget all {n} memories? This cannot be undone.Deutsch Alle {n} Einträge vergessen? Das lässt sich nicht rückgängig machen.
confirmForgetEnglisch Forget allDeutsch Alle vergessen
emptyEnglisch Nothing remembered yet. Facts appear here as you chat.Deutsch Noch nichts gemerkt. Fakten erscheinen hier, während du chattest.
noMatchEnglisch No memory matches your search.Deutsch Kein Eintrag passt zur Suche.
pausedEnglisch Memory is off. Existing memories stay until you delete them.Deutsch Gedächtnis ist aus. Bestehende Einträge bleiben, bis du sie löschst.
aboutEnglisch What should the assistant know about you?Deutsch Was soll der Assistent über dich wissen?
aboutHintEnglisch Role, company, what you work onDeutsch Rolle, Firma, woran du arbeitest
styleEnglisch How should it answer?Deutsch Wie soll er antworten?
styleHintEnglisch Length, tone, format, languageDeutsch Länge, Ton, Format, Sprache
presetsEnglisch Start from a presetDeutsch Mit einer Vorlage starten
previewEnglisch PreviewDeutsch Vorschau
previewNoteEnglisch A sample answer with these settingsDeutsch Eine Beispielantwort mit diesen Einstellungen
savedEnglisch Saved. New chats use these instructions.Deutsch Gespeichert. Neue Chats nutzen diese Anweisungen.
charsEnglisch {n} of {max}Deutsch {n} von {max}
unsavedEnglisch Unsaved changesDeutsch Nicht gespeichert
dataTitleEnglisch Your dataDeutsch Deine Daten
trainingEnglisch Improve the models with my chatsDeutsch Modelle mit meinen Chats verbessern
trainingNoteEnglisch Off by default. Chats are never used for training unless you turn this on.Deutsch Standardmäßig aus. Chats werden nie zum Training genutzt, außer du schaltest es ein.
historyEnglisch Keep chat historyDeutsch Chatverlauf behalten
historyNoteEnglisch Older chats are deleted automatically.Deutsch Ältere Chats werden automatisch gelöscht.
temporaryEnglisch Start temporary chats by defaultDeutsch Neue Chats vorübergehend starten
temporaryNoteEnglisch Temporary chats are not saved and not remembered.Deutsch Vorübergehende Chats werden nicht gespeichert und nicht gemerkt.
exportEnglisch Export my dataDeutsch Meine Daten exportieren
exportNoteEnglisch A file with all chats and memories, ready within a few minutes.Deutsch Eine Datei mit allen Chats und Einträgen, in wenigen Minuten fertig.
exportStartEnglisch Request exportDeutsch Export anfordern
exportReadyEnglisch Export requested. We email you the link.Deutsch Export angefordert. Wir mailen dir den Link.
deleteAllEnglisch Delete all chatsDeutsch Alle Chats löschen
deleteNoteEnglisch Removes every chat for good. Memories stay unless you forget them.Deutsch Entfernt jeden Chat endgültig. Das Gedächtnis bleibt, bis du es leerst.
deleteConfirmEnglisch Type DELETE to confirmDeutsch Zum Bestätigen LÖSCHEN eingeben
deleteWordEnglisch DELETEDeutsch LÖSCHEN
deleteDoneEnglisch All chats deleted.Deutsch Alle Chats gelöscht.
deleteEnglisch DeleteDeutsch Löschen
connectionsEnglisch Connected appsDeutsch Verbundene Apps
connectionsNoteEnglisch The assistant only reads what you allow here. Every access is listed with its last use.Deutsch Der Assistent liest nur, was du hier erlaubst. Jeder Zugriff steht mit seiner letzten Nutzung da.
connectEnglisch ConnectDeutsch Verbinden
disconnectEnglisch DisconnectDeutsch Trennen
lastUsedEnglisch Last used {when}Deutsch Zuletzt {when}
notConnectedEnglisch Not connectedDeutsch Nicht verbunden
accessEnglisch AccessDeutsch Zugriff
allowedEnglisch {on} of {n} allowedDeutsch {on} von {n} erlaubt
policyEnglisch Rules for the workspaceDeutsch Regeln für den Arbeitsbereich
policyNoteEnglisch Apply to everyone in the workspace. Members see which rule blocked a request.Deutsch Gelten für alle im Arbeitsbereich. Mitglieder sehen, welche Regel eine Anfrage gestoppt hat.
allowedModelsEnglisch Allowed modelsDeutsch Erlaubte Modelle
rulesTitleEnglisch ProtectionDeutsch Schutz
changedByEnglisch Changes are loggedDeutsch Änderungen werden protokolliert
publishEnglisch Apply rulesDeutsch Regeln anwenden
appliedEnglisch Rules applied to all members.Deutsch Regeln für alle Mitglieder angewendet.
onEnglisch OnDeutsch An
offEnglisch OffDeutsch Aus

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • KI-Einstellungen reagiert selbst auf die Tasten Escape, Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts, Entf und Leertaste.
  • Setzt aria-checked, aria-expanded, aria-label und aria-live, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rollen radio, radiogroup und status.
  • Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
  • Jedes Feld hat ein Label, sichtbar oder für Screenreader.
  • Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
  • Zeigt bei Bedienung mit der Tastatur einen sichtbaren Fokusring.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
  • Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 814 Zeilen
components/ui/ai-settings.tsx
'use client';

import { useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { Reveal } from '@/components/ui/reveal';
import { Stagger } from '@/components/ui/motion';
import { Switch } from '@/components/ui/switch';

export type AiSettingsVariant = 'memory' | 'instructions' | 'data-controls' | 'connected-apps' | 'workspace-policy';

export interface AiMemory {
  id: string;
  text: string;
  /** Where it was learned, e.g. "Chat on Oct 12" */
  source: string;
  group: string;
}

export interface AiTonePreset {
  id: string;
  label: string;
  /** How the sample answer reads with this preset */
  sample: string;
}

export interface AiConnection {
  id: string;
  name: string;
  icon?: IconName;
  /** What it gives the assistant, e.g. "Read your calendar" */
  access: { id: string; label: string; on: boolean }[];
  lastUsed?: string;
  connected: boolean;
}

export interface AiPolicyRule {
  id: string;
  label: string;
  description: string;
  icon?: IconName;
  on: boolean;
  /** Shown as a badge, e.g. "Blocks 12 requests a week" */
  impact?: string;
}

export interface AiSettingsProps {
  variant?: AiSettingsVariant;
  title?: string;
  description?: string;
  /** memory */
  memories?: AiMemory[];
  memoryOn?: boolean;
  /** instructions */
  about?: string;
  style?: string;
  presets?: AiTonePreset[];
  /** instructions: the question the sample answer replies to */
  sampleQuestion?: string;
  maxLength?: number;
  /** data-controls: how long chats are kept, the first is the default */
  retention?: string[];
  /** connected-apps */
  connections?: AiConnection[];
  /** workspace-policy */
  rules?: AiPolicyRule[];
  models?: { id: string; name: string; allowed: boolean }[];
  labels?: Partial<(typeof TEXTS)['en']>;
  className?: string;
}

const TEXTS = {
  en: {
    
    memoryOn: 'Remember things across chats',
    memoryNote: 'The assistant keeps short facts you share, so you do not have to repeat them. You see and control every one of them here.',
    search: 'Search memory',
    count: '{n} entries',
    learned: 'From {source}',
    edit: 'Edit {name}',
    save: 'Save',
    cancel: 'Cancel',
    forget: 'Forget {name}',
    forgotten: 'Forgotten. The assistant no longer uses it.',
    undo: 'Undo',
    forgetAll: 'Forget everything',
    forgetAllAsk: 'Forget all {n} memories? This cannot be undone.',
    confirmForget: 'Forget all',
    empty: 'Nothing remembered yet. Facts appear here as you chat.',
    noMatch: 'No memory matches your search.',
    paused: 'Memory is off. Existing memories stay until you delete them.',
    about: 'What should the assistant know about you?',
    aboutHint: 'Role, company, what you work on',
    style: 'How should it answer?',
    styleHint: 'Length, tone, format, language',
    presets: 'Start from a preset',
    preview: 'Preview',
    previewNote: 'A sample answer with these settings',
    saved: 'Saved. New chats use these instructions.',
    chars: '{n} of {max}',
    unsaved: 'Unsaved changes',
    dataTitle: 'Your data',
    training: 'Improve the models with my chats',
    trainingNote: 'Off by default. Chats are never used for training unless you turn this on.',
    history: 'Keep chat history',
    historyNote: 'Older chats are deleted automatically.',
    temporary: 'Start temporary chats by default',
    temporaryNote: 'Temporary chats are not saved and not remembered.',
    export: 'Export my data',
    exportNote: 'A file with all chats and memories, ready within a few minutes.',
    exportStart: 'Request export',
    exportReady: 'Export requested. We email you the link.',
    deleteAll: 'Delete all chats',
    deleteNote: 'Removes every chat for good. Memories stay unless you forget them.',
    deleteConfirm: 'Type DELETE to confirm',
    deleteWord: 'DELETE',
    deleteDone: 'All chats deleted.',
    delete: 'Delete',
    connections: 'Connected apps',
    connectionsNote: 'The assistant only reads what you allow here. Every access is listed with its last use.',
    connect: 'Connect',
    disconnect: 'Disconnect',
    lastUsed: 'Last used {when}',
    notConnected: 'Not connected',
    access: 'Access',
    allowed: '{on} of {n} allowed',
    policy: 'Rules for the workspace',
    policyNote: 'Apply to everyone in the workspace. Members see which rule blocked a request.',
    allowedModels: 'Allowed models',
    rulesTitle: 'Protection',
    changedBy: 'Changes are logged',
    publish: 'Apply rules',
    applied: 'Rules applied to all members.',
    on: 'On',
    off: 'Off',
  },
  de: {
    
    memoryOn: 'Über Chats hinweg merken',
    memoryNote: 'Der Assistent merkt sich kurze Fakten, die du teilst, damit du sie nicht wiederholen musst. Jeden davon siehst und steuerst du hier.',
    search: 'Gedächtnis durchsuchen',
    count: '{n} Einträge',
    learned: 'Aus {source}',
    edit: '{name} bearbeiten',
    save: 'Speichern',
    cancel: 'Abbrechen',
    forget: '{name} vergessen',
    forgotten: 'Vergessen. Der Assistent nutzt es nicht mehr.',
    undo: 'Rückgängig',
    forgetAll: 'Alles vergessen',
    forgetAllAsk: 'Alle {n} Einträge vergessen? Das lässt sich nicht rückgängig machen.',
    confirmForget: 'Alle vergessen',
    empty: 'Noch nichts gemerkt. Fakten erscheinen hier, während du chattest.',
    noMatch: 'Kein Eintrag passt zur Suche.',
    paused: 'Gedächtnis ist aus. Bestehende Einträge bleiben, bis du sie löschst.',
    about: 'Was soll der Assistent über dich wissen?',
    aboutHint: 'Rolle, Firma, woran du arbeitest',
    style: 'Wie soll er antworten?',
    styleHint: 'Länge, Ton, Format, Sprache',
    presets: 'Mit einer Vorlage starten',
    preview: 'Vorschau',
    previewNote: 'Eine Beispielantwort mit diesen Einstellungen',
    saved: 'Gespeichert. Neue Chats nutzen diese Anweisungen.',
    chars: '{n} von {max}',
    unsaved: 'Nicht gespeichert',
    dataTitle: 'Deine Daten',
    training: 'Modelle mit meinen Chats verbessern',
    trainingNote: 'Standardmäßig aus. Chats werden nie zum Training genutzt, außer du schaltest es ein.',
    history: 'Chatverlauf behalten',
    historyNote: 'Ältere Chats werden automatisch gelöscht.',
    temporary: 'Neue Chats vorübergehend starten',
    temporaryNote: 'Vorübergehende Chats werden nicht gespeichert und nicht gemerkt.',
    export: 'Meine Daten exportieren',
    exportNote: 'Eine Datei mit allen Chats und Einträgen, in wenigen Minuten fertig.',
    exportStart: 'Export anfordern',
    exportReady: 'Export angefordert. Wir mailen dir den Link.',
    deleteAll: 'Alle Chats löschen',
    deleteNote: 'Entfernt jeden Chat endgültig. Das Gedächtnis bleibt, bis du es leerst.',
    deleteConfirm: 'Zum Bestätigen LÖSCHEN eingeben',
    deleteWord: 'LÖSCHEN',
    deleteDone: 'Alle Chats gelöscht.',
    delete: 'Löschen',
    connections: 'Verbundene Apps',
    connectionsNote: 'Der Assistent liest nur, was du hier erlaubst. Jeder Zugriff steht mit seiner letzten Nutzung da.',
    connect: 'Verbinden',
    disconnect: 'Trennen',
    lastUsed: 'Zuletzt {when}',
    notConnected: 'Nicht verbunden',
    access: 'Zugriff',
    allowed: '{on} von {n} erlaubt',
    policy: 'Regeln für den Arbeitsbereich',
    policyNote: 'Gelten für alle im Arbeitsbereich. Mitglieder sehen, welche Regel eine Anfrage gestoppt hat.',
    allowedModels: 'Erlaubte Modelle',
    rulesTitle: 'Schutz',
    changedBy: 'Änderungen werden protokolliert',
    publish: 'Regeln anwenden',
    applied: 'Regeln für alle Mitglieder angewendet.',
    on: 'An',
    off: 'Aus',
  },
};

type Texts = (typeof TEXTS)['en'];
type P = AiSettingsProps & { t: Texts };
const fill = (s: string, v: Record<string, string | number>) => s.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ''));

/** State seeded from a prop that starts over when the prop changes (the Studio edits props live) */
function useSynced<T>(value: T) {
  const [state, setState] = useState(value);
  const key = JSON.stringify(value);
  const seen = useRef(key);
  if (seen.current !== key) {
    seen.current = key;
    setState(value);
  }
  return [state, setState] as const;
}

const CSS =
  '@keyframes ais-up{from{opacity:0;transform:translateY(8px)}}@keyframes ais-out{to{opacity:0;transform:translateX(20px)}}@keyframes ais-swap{from{opacity:0;filter:blur(3px)}}' +
  '@media (prefers-reduced-motion:no-preference){.ais-up{animation:ais-up .45s var(--ease-kit) var(--ais-d,0ms) both}.ais-out{animation:ais-out .3s var(--ease-kit) both}.ais-swap{animation:ais-swap .5s var(--ease-kit) both}}';

function Style() {
  return (
    <style href="kit-ai-settings" precedence="default">
      {CSS}
    </style>
  );
}

/* ------------------------------------------------------------------ */
/* Shared parts                                                        */
/* ------------------------------------------------------------------ */

function Stage({ title, description, children, className, wide }: { title?: string; description?: string; children: React.ReactNode; className?: string; wide?: boolean }) {
  return (
    <section className={cn('kit-glow relative overflow-clip bg-surface py-12 sm:py-16 lg:py-20', className)}>
      <div className="kit-container">
        <div className={cn('mx-auto w-full', wide ? 'max-w-[64rem]' : 'max-w-[46rem]')}>
          {(title || description) && (
            <Reveal className="mb-6 sm:mb-8">
              {title && <h2 className="text-balance text-[22px] font-semibold leading-tight tracking-[-0.02em] text-foreground sm:text-[26px]">{title}</h2>}
              {description && <p className="mt-2 max-w-[38rem] text-pretty text-[14.5px] leading-relaxed text-muted-foreground">{description}</p>}
            </Reveal>
          )}
          {children}
        </div>
      </div>
    </section>
  );
}

function Card({ children, className }: { children: React.ReactNode; className?: string }) {
  return <div className={cn('rounded-2xl border border-border bg-card shadow-[var(--highlight),var(--shadow-sm)]', className)}>{children}</div>;
}

const primaryBtn = 'inline-flex h-11 items-center justify-center gap-1.5 rounded-lg bg-primary px-4 text-[13.5px] font-medium text-primary-foreground shadow-[inset_0_1px_0_rgb(255_255_255/0.18),var(--shadow-sm)] transition-colors hover:bg-[color-mix(in_oklab,var(--primary)_88%,var(--background))] disabled:bg-[color-mix(in_oklab,var(--foreground)_8%,var(--card))] disabled:text-subtle-foreground disabled:shadow-none sm:h-9';
const quietBtn = 'inline-flex h-11 items-center justify-center gap-1.5 rounded-lg border border-border bg-card px-4 text-[13.5px] font-medium text-foreground transition-colors hover:bg-accent disabled:opacity-50 sm:h-9';
const dangerBtn = 'inline-flex h-11 items-center justify-center gap-1.5 rounded-lg border border-[color-mix(in_oklab,var(--destructive)_40%,var(--border))] px-4 text-[13.5px] font-medium text-destructive transition-colors hover:bg-[color-mix(in_oklab,var(--destructive)_10%,transparent)] sm:h-9';
const iconBtn = 'grid size-11 shrink-0 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-accent hover:text-foreground sm:size-8';
const field = 'w-full rounded-xl border border-border bg-background px-3.5 py-3 text-[14.5px] leading-relaxed text-foreground outline-none transition-[border-color,box-shadow] placeholder:text-subtle-foreground focus:border-[color-mix(in_oklab,var(--primary)_55%,var(--border))] focus:shadow-[0_0_0_4px_color-mix(in_oklab,var(--primary)_14%,transparent)] focus-visible:outline-none';

function Status({ children, tone = 'success' }: { children: React.ReactNode; tone?: 'success' | 'muted' }) {
  return (
    <span role="status" className={cn('ais-up inline-flex items-center gap-1.5 text-[13px]', tone === 'success' ? 'text-success-foreground' : 'text-muted-foreground')}>
      <Icon name={tone === 'success' ? 'check-circle' : 'info'} size={15} />
      {children}
    </span>
  );
}

/* ------------------------------------------------------------------ */
/* memory                                                              */
/* ------------------------------------------------------------------ */

function Memory({ t, title, description, memories = [], memoryOn = true, className }: P) {
  const [items, setItems] = useSynced(memories);
  const [on, setOn] = useState(memoryOn);
  const [q, setQ] = useState('');
  const [editing, setEditing] = useState<string | null>(null);
  const [draft, setDraft] = useState('');
  const [leaving, setLeaving] = useState<string | null>(null);
  const [last, setLast] = useState<{ m: AiMemory; i: number } | null>(null);
  const [ask, setAsk] = useState(false);
  const uid = useId();
  const shown = items.filter((m) => !q || m.text.toLowerCase().includes(q.toLowerCase()));
  const groups = Array.from(new Set(shown.map((m) => m.group)));

  const forget = (m: AiMemory) => {
    setLeaving(m.id);
    window.setTimeout(
      () => {
        setItems((s) => s.filter((x) => x.id !== m.id));
        setLast({ m, i: items.findIndex((x) => x.id === m.id) });
        setLeaving(null);
      },
      prefersReducedMotion() ? 0 : 300,
    );
  };

  return (
    <Stage title={title} description={description} className={className}>
      <Reveal delay={60}>
        <Card>
          <div className="border-b border-border p-4 sm:p-5">
            <Switch label={<span className="text-[14.5px] font-medium">{t.memoryOn}</span>} description={<span className="text-[13px] leading-relaxed">{t.memoryNote}</span>} checked={on} onCheckedChange={setOn} />
          </div>
          {!on && (
            <p className="ais-up flex items-center gap-2 border-b border-border bg-[color-mix(in_oklab,var(--warning)_10%,var(--card))] px-4 py-2.5 text-[13px] text-warning-foreground sm:px-5">
              <Icon name="pause" size={14} />
              {t.paused}
            </p>
          )}
          <div className={cn('transition-opacity duration-300', !on && 'opacity-60')}>
            <div className="border-b border-border p-3 sm:px-5">
              <label className="flex h-11 min-w-0 items-center gap-2 rounded-lg border border-border bg-background px-3 focus-within:border-[color-mix(in_oklab,var(--primary)_55%,var(--border))] focus-within:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_16%,transparent)] sm:h-9">
                <Icon name="search" size={15} className="shrink-0 text-muted-foreground" />
                <span className="sr-only">{t.search}</span>
                <input value={q} onChange={(e) => setQ(e.target.value)} placeholder={t.search} className="w-0 min-w-0 flex-1 bg-transparent text-[14px] text-foreground outline-none placeholder:text-subtle-foreground focus-visible:outline-none" />
                <span className="shrink-0 rounded-md bg-[color-mix(in_oklab,var(--foreground)_7%,transparent)] px-1.5 py-0.5 text-[12px] tabular-nums text-muted-foreground" aria-label={fill(t.count, { n: shown.length })}>
                  {shown.length}
                </span>
              </label>
            </div>
            {last && (
              <p role="status" className="ais-up flex items-center justify-between gap-3 border-b border-border bg-surface/60 px-4 py-2 text-[13px] text-muted-foreground sm:px-5">
                {t.forgotten}
                <button type="button" onClick={() => (setItems((s) => [...s.slice(0, last.i), last.m, ...s.slice(last.i)]), setLast(null))} className="-my-1 inline-flex h-11 items-center rounded-md px-2 font-medium text-foreground hover:bg-accent sm:h-8">
                  {t.undo}
                </button>
              </p>
            )}
            {items.length === 0 ? (
              <p className="px-5 py-10 text-center text-[13.5px] text-muted-foreground">{t.empty}</p>
            ) : shown.length === 0 ? (
              <p className="px-5 py-10 text-center text-[13.5px] text-muted-foreground">{t.noMatch}</p>
            ) : (
              <div className="divide-y divide-border">
                {groups.map((g) => (
                  <div key={g} className="py-2">
                    <p className="px-4 pb-1 pt-2 text-[12px] font-medium text-subtle-foreground sm:px-5">{g}</p>
                    <Stagger as="ul" step={50}>
                      {shown
                        .filter((m) => m.group === g)
                        .map((m) => (
                          <li key={m.id} className={cn('group flex items-start gap-3 px-4 py-2.5 sm:px-5', leaving === m.id && 'ais-out')}>
                            <span aria-hidden className="mt-2 size-1.5 shrink-0 rounded-full bg-primary" />
                            {editing === m.id ? (
                              <form
                                className="min-w-0 flex-1"
                                onSubmit={(e) => {
                                  e.preventDefault();
                                  setItems((s) => s.map((x) => (x.id === m.id ? { ...x, text: draft.trim() || x.text } : x)));
                                  setEditing(null);
                                }}
                              >
                                <label className="sr-only" htmlFor={`${uid}-mem-${m.id}`}>
                                  {fill(t.edit, { name: m.text })}
                                </label>
                                <input id={`${uid}-mem-${m.id}`} autoFocus value={draft} onChange={(e) => setDraft(e.target.value)} onKeyDown={(e) => e.key === 'Escape' && setEditing(null)} className={cn(field, 'h-11 py-0 sm:h-9')} />
                                <div className="mt-2 flex gap-2">
                                  <button type="submit" className={primaryBtn}>
                                    {t.save}
                                  </button>
                                  <button type="button" onClick={() => setEditing(null)} className={quietBtn}>
                                    {t.cancel}
                                  </button>
                                </div>
                              </form>
                            ) : (
                              <>
                                <span className="min-w-0 flex-1">
                                  <span className="block text-pretty text-[14.5px] leading-snug text-foreground">{m.text}</span>
                                  <span className="mt-0.5 block text-[12px] text-muted-foreground">{fill(t.learned, { source: m.source })}</span>
                                </span>
                                <span className="-my-1 flex shrink-0 opacity-100 transition-opacity sm:opacity-0 sm:group-focus-within:opacity-100 sm:group-hover:opacity-100">
                                  <button type="button" aria-label={fill(t.edit, { name: m.text })} onClick={() => (setEditing(m.id), setDraft(m.text))} className={iconBtn}>
                                    <Icon name="edit" size={15} />
                                  </button>
                                  <button type="button" aria-label={fill(t.forget, { name: m.text })} onClick={() => forget(m)} className={cn(iconBtn, 'hover:text-destructive')}>
                                    <Icon name="trash" size={15} />
                                  </button>
                                </span>
                              </>
                            )}
                          </li>
                        ))}
                    </Stagger>
                  </div>
                ))}
              </div>
            )}
          </div>
          <div className="border-t border-border bg-surface/60 px-4 py-3 sm:px-5">
            {ask ? (
              <div className="ais-up flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
                <p className="text-[13.5px] text-foreground">{fill(t.forgetAllAsk, { n: items.length })}</p>
                <div className="flex gap-2">
                  <button type="button" onClick={() => setAsk(false)} className={quietBtn}>
                    {t.cancel}
                  </button>
                  <button type="button" onClick={() => (setItems([]), setAsk(false), setLast(null))} className="inline-flex h-11 items-center rounded-lg bg-destructive px-4 text-[13.5px] font-medium text-destructive-foreground sm:h-9">
                    {t.confirmForget}
                  </button>
                </div>
              </div>
            ) : (
              <button type="button" disabled={!items.length} onClick={() => setAsk(true)} className={cn(dangerBtn, 'disabled:opacity-40')}>
                <Icon name="trash" size={15} />
                {t.forgetAll}
              </button>
            )}
          </div>
        </Card>
      </Reveal>
    </Stage>
  );
}

/* ------------------------------------------------------------------ */
/* instructions                                                        */
/* ------------------------------------------------------------------ */

function Instructions({ t, title, description, about = '', style = '', presets = [], sampleQuestion, maxLength = 1500, className }: P) {
  const [a, setA] = useSynced(about);
  const [s, setS] = useSynced(style);
  const [preset, setPreset] = useState(presets[0]?.id);
  const [saved, setSaved] = useState({ a: about, s: style, preset: presets[0]?.id });
  const [flash, setFlash] = useState(false);
  const dirty = a !== saved.a || s !== saved.s || preset !== saved.preset;
  const cur = presets.find((p) => p.id === preset);
  const id = useId();
  const refs = useRef<(HTMLButtonElement | null)[]>([]);

  const count = (v: string) => (
    <span className={cn('tabular-nums', v.length > maxLength * 0.9 ? 'text-warning-foreground' : 'text-subtle-foreground')}>{fill(t.chars, { n: v.length, max: maxLength })}</span>
  );

  return (
    <Stage title={title} description={description} className={className} wide>
      <div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] lg:items-start lg:gap-6">
        <Reveal delay={60}>
          <Card>
            <form
              className="space-y-5 p-4 sm:p-5"
              onSubmit={(e) => {
                e.preventDefault();
                setSaved({ a, s, preset });
                setFlash(true);
              }}
            >
              <div>
                <label htmlFor={`${id}-a`} className="block text-[14px] font-medium text-foreground">
                  {t.about}
                </label>
                <p className="mb-2 mt-0.5 text-[12.5px] text-muted-foreground">{t.aboutHint}</p>
                <textarea id={`${id}-a`} rows={4} maxLength={maxLength} value={a} onChange={(e) => (setA(e.target.value), setFlash(false))} className={cn(field, 'resize-none')} />
                <p className="mt-1 text-right text-[12px]">{count(a)}</p>
              </div>
              <div>
                <label htmlFor={`${id}-s`} className="block text-[14px] font-medium text-foreground">
                  {t.style}
                </label>
                <p className="mb-2 mt-0.5 text-[12.5px] text-muted-foreground">{t.styleHint}</p>
                <textarea id={`${id}-s`} rows={3} maxLength={maxLength} value={s} onChange={(e) => (setS(e.target.value), setFlash(false))} className={cn(field, 'resize-none')} />
                <p className="mt-1 text-right text-[12px]">{count(s)}</p>
              </div>
              {presets.length > 0 && (
                <div role="radiogroup" aria-label={t.presets}>
                  <p className="mb-2 text-[12.5px] font-medium text-muted-foreground">{t.presets}</p>
                  <div className="flex flex-wrap gap-2">
                    {presets.map((p, i) => {
                      const on = p.id === preset;
                      return (
                        <button
                          key={p.id}
                          ref={(el) => {
                            refs.current[i] = el;
                          }}
                          type="button"
                          role="radio"
                          aria-checked={on}
                          tabIndex={on ? 0 : -1}
                          onClick={() => (setPreset(p.id), setFlash(false))}
                          onKeyDown={(e) => {
                            const d = ['ArrowRight', 'ArrowDown'].includes(e.key) ? 1 : ['ArrowLeft', 'ArrowUp'].includes(e.key) ? -1 : 0;
                            if (!d) return;
                            e.preventDefault();
                            const n = (i + d + presets.length) % presets.length;
                            setPreset(presets[n].id);
                            refs.current[n]?.focus();
                          }}
                          className={cn('inline-flex min-h-11 items-center rounded-lg border px-3 text-[13px] font-medium transition-colors sm:min-h-9', on ? 'border-[color-mix(in_oklab,var(--primary)_50%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_12%,var(--card))] text-foreground' : 'border-border text-muted-foreground hover:text-foreground')}
                        >
                          {p.label}
                        </button>
                      );
                    })}
                  </div>
                </div>
              )}
              <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border pt-4">
                <span aria-live="polite">{flash ? <Status>{t.saved}</Status> : dirty ? <Status tone="muted">{t.unsaved}</Status> : null}</span>
                <div className="flex gap-2">
                  <button type="button" disabled={!dirty} onClick={() => (setA(saved.a), setS(saved.s), setPreset(saved.preset))} className={quietBtn}>
                    {t.cancel}
                  </button>
                  <button type="submit" disabled={!dirty} className={primaryBtn}>
                    {t.save}
                  </button>
                </div>
              </div>
            </form>
          </Card>
        </Reveal>
        <Reveal delay={160} className="lg:sticky lg:top-6">
          <Card className="overflow-hidden">
            <div className="border-b border-border px-4 py-3 sm:px-5">
              <p className="text-[13px] font-medium text-foreground">{t.preview}</p>
              <p className="text-[12px] text-muted-foreground">{t.previewNote}</p>
            </div>
            <div className="flex flex-wrap gap-1.5 border-b border-border px-4 py-2.5 sm:px-5">
              {[a, s]
                .filter(Boolean)
                .map((v, i) => (
                  <span key={i} className="max-w-full truncate rounded-md bg-[color-mix(in_oklab,var(--primary)_10%,transparent)] px-2 py-1 text-[11.5px] text-[color-mix(in_oklab,var(--primary)_70%,var(--foreground))]">
                    {v.length > 46 ? `${v.slice(0, 44)}…` : v}
                  </span>
                ))}
            </div>
            <div className="space-y-3 p-4 sm:p-5">
              {sampleQuestion && <p className="ml-auto w-fit max-w-[90%] rounded-2xl rounded-br-md bg-[color-mix(in_oklab,var(--primary)_12%,var(--card))] px-3.5 py-2.5 text-[14px] text-foreground">{sampleQuestion}</p>}
              <div className="flex gap-2.5">
                <span aria-hidden className="grid size-7 shrink-0 place-items-center rounded-full bg-[color-mix(in_oklab,var(--primary)_14%,var(--card))] text-primary">
                  <Icon name="sparkle" size={14} />
                </span>
                <p key={preset} className="ais-swap whitespace-pre-line text-pretty text-[14px] leading-relaxed text-foreground">
                  {cur?.sample}
                </p>
              </div>
            </div>
          </Card>
        </Reveal>
      </div>
    </Stage>
  );
}

/* ------------------------------------------------------------------ */
/* data-controls                                                       */
/* ------------------------------------------------------------------ */

function DataControls({ t, title, description, retention = [], className }: P) {
  const [keep, setKeep] = useState(retention[0] ?? '');
  const [exported, setExported] = useState(false);
  const [del, setDel] = useState<'idle' | 'ask' | 'done'>('idle');
  const [word, setWord] = useState('');
  const id = useId();
  const row = 'flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:justify-between sm:gap-6 sm:px-5';
  return (
    <Stage title={title} description={description} className={className}>
      <Stagger step={90} className="space-y-4">
        <Card>
          <p className="border-b border-border px-4 py-3 text-[13px] font-medium text-foreground sm:px-5">{t.dataTitle}</p>
          <div className="divide-y divide-border">
            <div className="px-4 py-4 sm:px-5">
              <Switch label={<span className="text-[14.5px] font-medium">{t.training}</span>} description={<span className="text-[13px] leading-relaxed">{t.trainingNote}</span>} defaultChecked={false} />
            </div>
            <div className={row}>
              <div className="min-w-0">
                <label htmlFor={`${id}-keep`} className="block text-[14.5px] font-medium text-foreground">
                  {t.history}
                </label>
                <p className="mt-0.5 text-[13px] leading-relaxed text-muted-foreground">{t.historyNote}</p>
              </div>
              <span className="relative shrink-0 sm:w-44">
                <select id={`${id}-keep`} value={keep} onChange={(e) => setKeep(e.target.value)} className="h-11 w-full appearance-none rounded-lg border border-border bg-background pl-3 pr-9 text-[14px] text-foreground hover:border-border-strong sm:h-9">
                  {retention.map((r) => (
                    <option key={r}>{r}</option>
                  ))}
                </select>
                <Icon name="chevrons-up-down" size={15} className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
              </span>
            </div>
            <div className="px-4 py-4 sm:px-5">
              <Switch label={<span className="text-[14.5px] font-medium">{t.temporary}</span>} description={<span className="text-[13px] leading-relaxed">{t.temporaryNote}</span>} />
            </div>
          </div>
        </Card>
        <Card>
          <div className={row}>
            <div className="min-w-0">
              <p className="text-[14.5px] font-medium text-foreground">{t.export}</p>
              <p className="mt-0.5 text-[13px] leading-relaxed text-muted-foreground">{t.exportNote}</p>
            </div>
            {exported ? (
              <Status>{t.exportReady}</Status>
            ) : (
              <button type="button" onClick={() => setExported(true)} className={cn(quietBtn, 'shrink-0 self-start sm:self-auto')}>
                <Icon name="download" size={15} />
                {t.exportStart}
              </button>
            )}
          </div>
        </Card>
        <Card className="border-[color-mix(in_oklab,var(--destructive)_30%,var(--border))]">
          <div className={row}>
            <div className="min-w-0">
              <p className="text-[14.5px] font-medium text-foreground">{t.deleteAll}</p>
              <p className="mt-0.5 text-[13px] leading-relaxed text-muted-foreground">{t.deleteNote}</p>
            </div>
            {del === 'idle' && (
              <button type="button" onClick={() => setDel('ask')} className={cn(dangerBtn, 'shrink-0 self-start sm:self-auto')}>
                <Icon name="trash" size={15} />
                {t.deleteAll}
              </button>
            )}
            {del === 'done' && <Status>{t.deleteDone}</Status>}
          </div>
          {del === 'ask' && (
            <form
              className="ais-up flex flex-col gap-2 border-t border-border px-4 py-4 sm:flex-row sm:items-end sm:px-5"
              onSubmit={(e) => {
                e.preventDefault();
                if (word === t.deleteWord) setDel('done');
              }}
            >
              <label className="min-w-0 flex-1">
                <span className="mb-1.5 block text-[13px] text-muted-foreground">{t.deleteConfirm}</span>
                <input autoFocus value={word} onChange={(e) => setWord(e.target.value)} className={cn(field, 'h-11 py-0 sm:h-9')} />
              </label>
              <div className="flex gap-2">
                <button type="button" onClick={() => (setDel('idle'), setWord(''))} className={quietBtn}>
                  {t.cancel}
                </button>
                <button type="submit" disabled={word !== t.deleteWord} className="inline-flex h-11 items-center rounded-lg bg-destructive px-4 text-[13.5px] font-medium text-destructive-foreground disabled:opacity-40 sm:h-9">
                  {t.delete}
                </button>
              </div>
            </form>
          )}
        </Card>
      </Stagger>
    </Stage>
  );
}

/* ------------------------------------------------------------------ */
/* connected-apps                                                      */
/* ------------------------------------------------------------------ */

function ConnectedApps({ t, title, description, connections = [], className }: P) {
  const [items, setItems] = useSynced(connections);
  const [open, setOpen] = useState<string | null>(connections.find((c) => c.connected)?.id ?? null);
  const toggle = (cid: string, aid: string) => setItems((s) => s.map((c) => (c.id === cid ? { ...c, access: c.access.map((a) => (a.id === aid ? { ...a, on: !a.on } : a)) } : c)));
  return (
    <Stage title={title ?? t.connections} description={description ?? t.connectionsNote} className={className}>
      <Stagger as="ul" step={70} className="space-y-3">
        {items.map((c) => {
          const opened = open === c.id && c.connected;
          const on = c.access.filter((a) => a.on).length;
          return (
            <li key={c.id}>
              <Card className={cn('overflow-hidden transition-[border-color] duration-300', opened && 'border-border-strong')}>
                <div className="flex items-center gap-3 p-4 sm:px-5">
                  <span aria-hidden className={cn('grid size-11 shrink-0 place-items-center rounded-xl border border-border', c.connected ? 'bg-background text-foreground' : 'bg-surface text-muted-foreground')}>
                    <Icon name={c.icon ?? 'plug'} size={20} />
                  </span>
                  <div className="min-w-0 flex-1">
                    <p className="flex items-center gap-2 text-[15px] font-medium text-foreground">
                      {c.name}
                      {c.connected && <span aria-hidden className="size-2 rounded-full bg-success" />}
                    </p>
                    <p className="truncate text-[12.5px] text-muted-foreground">{c.connected ? `${fill(t.allowed, { on, n: c.access.length })}${c.lastUsed ? ` · ${fill(t.lastUsed, { when: c.lastUsed })}` : ''}` : t.notConnected}</p>
                  </div>
                  {c.connected ? (
                    <button type="button" aria-expanded={opened} aria-label={`${t.access}, ${c.name}`} onClick={() => setOpen(opened ? null : c.id)} className={iconBtn}>
                      <Icon name="chevron-down" size={17} className={cn('transition-transform duration-300', opened && 'rotate-180')} />
                    </button>
                  ) : (
                    <button type="button" onClick={() => (setItems((s) => s.map((x) => (x.id === c.id ? { ...x, connected: true } : x))), setOpen(c.id))} className={cn(quietBtn, 'shrink-0')}>
                      <Icon name="plug" size={15} />
                      {t.connect}
                    </button>
                  )}
                </div>
                <div className={cn('grid transition-[grid-template-rows] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', opened ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]')}>
                  <div className="overflow-hidden" inert={!opened || undefined}>
                    <div className="border-t border-border px-4 py-2 sm:px-5">
                      <ul className="divide-y divide-border">
                        {c.access.map((a) => (
                          <li key={a.id} className="py-1">
                            <Switch size="sm" label={<span className="text-[13.5px]">{a.label}</span>} checked={a.on} onCheckedChange={() => toggle(c.id, a.id)} className="min-h-11" />
                          </li>
                        ))}
                      </ul>
                      <div className="flex justify-end py-3">
                        <button type="button" onClick={() => (setItems((s) => s.map((x) => (x.id === c.id ? { ...x, connected: false } : x))), setOpen(null))} className={dangerBtn}>
                          {t.disconnect}
                        </button>
                      </div>
                    </div>
                  </div>
                </div>
              </Card>
            </li>
          );
        })}
      </Stagger>
    </Stage>
  );
}

/* ------------------------------------------------------------------ */
/* workspace-policy                                                    */
/* ------------------------------------------------------------------ */

function WorkspacePolicy({ t, title, description, rules = [], models = [], className }: P) {
  const [r, setR] = useSynced(rules);
  const [m, setM] = useSynced(models);
  const [applied, setApplied] = useState(false);
  const first = useRef(true);
  useEffect(() => {
    if (first.current) return void (first.current = false);
    setApplied(false);
  }, [r, m]);
  return (
    <Stage title={title ?? t.policy} description={description ?? t.policyNote} className={className} wide>
      <div className="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)] lg:items-start lg:gap-6">
        <Reveal delay={60}>
          <Card>
            <p className="border-b border-border px-4 py-3 text-[13px] font-medium text-foreground sm:px-5">{t.rulesTitle}</p>
            <Stagger as="ul" step={60} className="divide-y divide-border">
              {r.map((x) => (
                <li key={x.id} className="flex gap-3 px-4 py-4 sm:px-5">
                  <span aria-hidden className={cn('grid size-10 shrink-0 place-items-center rounded-xl transition-colors duration-300', x.on ? 'bg-[color-mix(in_oklab,var(--primary)_14%,var(--card))] text-primary' : 'bg-surface text-muted-foreground')}>
                    <Icon name={x.icon ?? 'shield'} size={18} />
                  </span>
                  <div className="min-w-0 flex-1">
                    <Switch label={<span className="text-[14.5px] font-medium">{x.label}</span>} description={<span className="text-[13px] leading-relaxed">{x.description}</span>} checked={x.on} onCheckedChange={(v) => setR((s) => s.map((y) => (y.id === x.id ? { ...y, on: v } : y)))} />
                    {x.impact && x.on && <span className="ais-up mt-2 inline-flex rounded-md bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] px-2 py-1 text-[12px] text-muted-foreground">{x.impact}</span>}
                  </div>
                </li>
              ))}
            </Stagger>
          </Card>
        </Reveal>
        <Reveal delay={160} className="space-y-4 lg:sticky lg:top-6">
          <Card>
            <p className="border-b border-border px-4 py-3 text-[13px] font-medium text-foreground sm:px-5">{t.allowedModels}</p>
            <ul className="p-2">
              {m.map((x) => (
                <li key={x.id}>
                  <label className="flex min-h-11 cursor-pointer items-center gap-3 rounded-lg px-2.5 hover:bg-accent/60">
                    <input type="checkbox" checked={x.allowed} onChange={() => setM((s) => s.map((y) => (y.id === x.id ? { ...y, allowed: !y.allowed } : y)))} className="size-[18px] accent-[var(--primary)]" />
                    <span className={cn('text-[14px]', x.allowed ? 'text-foreground' : 'text-muted-foreground')}>{x.name}</span>
                    <span className="ml-auto text-[12px] text-subtle-foreground">{x.allowed ? t.on : t.off}</span>
                  </label>
                </li>
              ))}
            </ul>
          </Card>
          <Card className="p-4 sm:p-5">
            <p className="flex items-center gap-1.5 text-[12.5px] text-muted-foreground">
              <Icon name="history" size={14} />
              {t.changedBy}
            </p>
            <div className="mt-3" aria-live="polite">
              {applied ? (
                <Status>{t.applied}</Status>
              ) : (
                <button type="button" onClick={() => setApplied(true)} className={cn(primaryBtn, 'w-full')}>
                  {t.publish}
                </button>
              )}
            </div>
          </Card>
        </Reveal>
      </div>
    </Stage>
  );
}

/* ------------------------------------------------------------------ */

/**
 * Settings of an AI assistant: memory you can see and edit, custom
 * instructions with a live preview, data controls with export and delete,
 * connected apps with access per scope and rules for a whole workspace.
 */
export function AiSettings({ variant = 'memory', labels, ...props }: AiSettingsProps) {
  const t = useTexts(TEXTS, labels);
  const p: P = { ...props, t };
  const body = (() => {
    switch (variant) {
      case 'instructions':
        return <Instructions {...p} />;
      case 'data-controls':
        return <DataControls {...p} />;
      case 'connected-apps':
        return <ConnectedApps {...p} />;
      case 'workspace-policy':
        return <WorkspacePolicy {...p} />;
      default:
        return <Memory {...p} />;
    }
  })();
  return (
    <>
      <Style />
      {body}
    </>
  );
}

Häufige Fragen

Wie installiere ich KI-Einstellungen?

Mit aiw add @ai-website/ai-settings. Der Befehl legt die Datei components/ui/ai-settings.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, reveal, motion und switch. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat KI-Einstellungen?

5 Varianten, Gedächtnis, Eigene Anweisungen, Datenkontrolle, Verbundene Apps und Regeln für das Team. Du wählst sie mit der Prop variant, zum Beispiel variant="instructions".

Kann ich die Texte von KI-Einstellungen übersetzen?

Ja. KI-Einstellungen bringt 61 eigene Texte auf Deutsch und Englisch mit, etwa „Über Chats hinweg merken“, „Der Assistent merkt sich kurze Fakten, die du teilst, damit du sie nicht wiederholen musst. Jeden davon siehst und steuerst du hier.“ und „Gedächtnis durchsuchen“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent KI-Einstellungen einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini KI-Einstellungen mit aiw_components und installieren sie mit aiw_add @ai-website/ai-settings. Nenn im Prompt die Variante, etwa variant="memory", dann schreibt der Assistent deine Inhalte direkt in die Props.

Kann ich KI-Einstellungen ohne Code anpassen?

Ja, im Studio. Alle 5 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden AiSettings-Code zum Kopieren.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert