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
Variants
Details
Was KI-Einstellungen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- KI-Einrichtung
- Bereich
- App
- Variants
- 5
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/ai-settings.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 61 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle AiSettingsProps aus components/ui/ai-settings.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalAiSettingsVariantmemoryinstructionsdata-controlsconnected-appsworkspace-policy
titleoptionalstringdescriptionoptionalstringmemoriesoptionalAiMemory[]memory
memoryOnoptionalbooleanaboutoptionalstringinstructions
styleoptionalstringpresetsoptionalAiTonePreset[]sampleQuestionoptionalstringinstructions: the question the sample answer replies to
maxLengthoptionalnumberretentionoptionalstring[]data-controls: how long chats are kept, the first is the default
connectionsoptionalAiConnection[]connected-apps
rulesoptionalAiPolicyRule[]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
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| memoryOn | Englisch Remember things across chats | Deutsch Über Chats hinweg merken |
| memoryNote | Englisch 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. |
| search | Englisch Search memory | Deutsch Gedächtnis durchsuchen |
| count | Englisch {n} entries | Deutsch {n} Einträge |
| learned | Englisch From {source} | Deutsch Aus {source} |
| edit | Englisch Edit {name} | Deutsch {name} bearbeiten |
| save | Englisch Save | Deutsch Speichern |
| cancel | Englisch Cancel | Deutsch Abbrechen |
| forget | Englisch Forget {name} | Deutsch {name} vergessen |
| forgotten | Englisch Forgotten. The assistant no longer uses it. | Deutsch Vergessen. Der Assistent nutzt es nicht mehr. |
| undo | Englisch Undo | Deutsch Rückgängig |
| forgetAll | Englisch Forget everything | Deutsch Alles vergessen |
| forgetAllAsk | Englisch Forget all {n} memories? This cannot be undone. | Deutsch Alle {n} Einträge vergessen? Das lässt sich nicht rückgängig machen. |
| confirmForget | Englisch Forget all | Deutsch Alle vergessen |
| empty | Englisch Nothing remembered yet. Facts appear here as you chat. | Deutsch Noch nichts gemerkt. Fakten erscheinen hier, während du chattest. |
| noMatch | Englisch No memory matches your search. | Deutsch Kein Eintrag passt zur Suche. |
| paused | Englisch Memory is off. Existing memories stay until you delete them. | Deutsch Gedächtnis ist aus. Bestehende Einträge bleiben, bis du sie löschst. |
| about | Englisch What should the assistant know about you? | Deutsch Was soll der Assistent über dich wissen? |
| aboutHint | Englisch Role, company, what you work on | Deutsch Rolle, Firma, woran du arbeitest |
| style | Englisch How should it answer? | Deutsch Wie soll er antworten? |
| styleHint | Englisch Length, tone, format, language | Deutsch Länge, Ton, Format, Sprache |
| presets | Englisch Start from a preset | Deutsch Mit einer Vorlage starten |
| preview | Englisch Preview | Deutsch Vorschau |
| previewNote | Englisch A sample answer with these settings | Deutsch Eine Beispielantwort mit diesen Einstellungen |
| saved | Englisch Saved. New chats use these instructions. | Deutsch Gespeichert. Neue Chats nutzen diese Anweisungen. |
| chars | Englisch {n} of {max} | Deutsch {n} von {max} |
| unsaved | Englisch Unsaved changes | Deutsch Nicht gespeichert |
| dataTitle | Englisch Your data | Deutsch Deine Daten |
| training | Englisch Improve the models with my chats | Deutsch Modelle mit meinen Chats verbessern |
| trainingNote | Englisch 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. |
| history | Englisch Keep chat history | Deutsch Chatverlauf behalten |
| historyNote | Englisch Older chats are deleted automatically. | Deutsch Ältere Chats werden automatisch gelöscht. |
| temporary | Englisch Start temporary chats by default | Deutsch Neue Chats vorübergehend starten |
| temporaryNote | Englisch Temporary chats are not saved and not remembered. | Deutsch Vorübergehende Chats werden nicht gespeichert und nicht gemerkt. |
| export | Englisch Export my data | Deutsch Meine Daten exportieren |
| exportNote | Englisch 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. |
| exportStart | Englisch Request export | Deutsch Export anfordern |
| exportReady | Englisch Export requested. We email you the link. | Deutsch Export angefordert. Wir mailen dir den Link. |
| deleteAll | Englisch Delete all chats | Deutsch Alle Chats löschen |
| deleteNote | Englisch 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. |
| deleteConfirm | Englisch Type DELETE to confirm | Deutsch Zum Bestätigen LÖSCHEN eingeben |
| deleteWord | Englisch DELETE | Deutsch LÖSCHEN |
| deleteDone | Englisch All chats deleted. | Deutsch Alle Chats gelöscht. |
| delete | Englisch Delete | Deutsch Löschen |
| connections | Englisch Connected apps | Deutsch Verbundene Apps |
| connectionsNote | Englisch 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. |
| connect | Englisch Connect | Deutsch Verbinden |
| disconnect | Englisch Disconnect | Deutsch Trennen |
| lastUsed | Englisch Last used {when} | Deutsch Zuletzt {when} |
| notConnected | Englisch Not connected | Deutsch Nicht verbunden |
| access | Englisch Access | Deutsch Zugriff |
| allowed | Englisch {on} of {n} allowed | Deutsch {on} von {n} erlaubt |
| policy | Englisch Rules for the workspace | Deutsch Regeln für den Arbeitsbereich |
| policyNote | Englisch 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. |
| allowedModels | Englisch Allowed models | Deutsch Erlaubte Modelle |
| rulesTitle | Englisch Protection | Deutsch Schutz |
| changedBy | Englisch Changes are logged | Deutsch Änderungen werden protokolliert |
| publish | Englisch Apply rules | Deutsch Regeln anwenden |
| applied | Englisch Rules applied to all members. | Deutsch Regeln für alle Mitglieder angewendet. |
| on | Englisch On | Deutsch An |
| off | Englisch Off | Deutsch 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
'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.