KI-Einstieg
Die ersten Minuten mit einem KI-Assistenten. Was er kann, mit einem Beispiel, das sich selbst tippt, eine erste Aufgabe zum Starten, die Datenquellen zum Verbinden mit dem, was jede liest, und eine ehrliche Seite zu Grenzen und Versprechen.
Eine von 3 Components in KI-Einrichtung
Variants
Details
Was KI-Einstieg mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- KI-Einrichtung
- Bereich
- App
- Variants
- 4
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/ai-onboarding.tsx
- Bringt mit
- utilskiticonrevealmotion
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 21 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle AiOnboardingProps aus components/ui/ai-onboarding.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalAiOnboardingVariantcapabilitiesfirst-taskconnect-sourceslimits-promise
nameoptionalstringThe assistant's name
titleoptionalstringdescriptionoptionalstringcapabilitiesoptionalAiCapability[]tasksoptionalAiStarterTask[]sourcesoptionalAiSource[]canoptionalstring[]limits-promise: what it does well and what it does not
cannotoptionalstring[]promisesoptionalAiPromiseItem[]stepoptional{ current: number; total: number }Step shown in the progress line, e.g. { current: 2, total: 3 }
onNextoptional() => voidContinue, the step is marked done and the next segment fills
onBackoptional() => voidBack and Skip only show when they are handled
onSkipoptional() => voidonStartoptional(task: string) => voidfirst-task: the chosen task or the own words
Eingebaute Texte
Diese Texte schreibt KI-Einstieg selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 21 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| step | Englisch Step {n} of {total} | Deutsch Schritt {n} von {total} |
| next | Englisch Continue | Deutsch Weiter |
| back | Englisch Back | Deutsch Zurück |
| skip | Englisch Skip for now | Deutsch Später |
| request | Englisch You ask | Deutsch Du fragst |
| result | Englisch Result | Deutsch Ergebnis |
| start | Englisch Start with this | Deutsch Damit starten |
| started | Englisch Your first chat starts with this task. | Deutsch Dein erster Chat startet mit dieser Aufgabe. |
| stepDone | Englisch Step {n} done | Deutsch Schritt {n} erledigt |
| needs | Englisch Needs | Deutsch Braucht |
| connect | Englisch Connect | Deutsch Verbinden |
| connected | Englisch Connected | Deutsch Verbunden |
| reads | Englisch Reads | Deutsch Liest |
| later | Englisch You can connect more later in settings. | Deutsch Weitere Quellen kannst du später in den Einstellungen verbinden. |
| count | Englisch {n} of {total} connected | Deutsch {n} von {total} verbunden |
| can | Englisch Good at | Deutsch Gut darin |
| cannot | Englisch Not made for | Deutsch Nicht gemacht für |
| promise | Englisch Our promise | Deutsch Unser Versprechen |
| agree | Englisch Understood, start now | Deutsch Verstanden, jetzt starten |
| custom | Englisch Or describe your own task | Deutsch Oder beschreib deine eigene Aufgabe |
| customPlaceholder | Englisch Plan the team day in November | Deutsch Plane den Teamtag im November |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- KI-Einstieg reagiert selbst auf die Tasten Pfeil hoch, Pfeil runter, Pfeil links, Pfeil rechts und Leertaste.
- Setzt aria-checked, aria-label, aria-live, aria-orientation, aria-selected, aria-valuemax, aria-valuemin und aria-valuenow, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen progressbar, radio, radiogroup, status, tab, tablist und tabpanel.
- 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, 570 Zeilen
'use client';
import { useEffect, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useInView, useTexts } from '@/lib/kit';
import { Icon, type IconName } from '@/components/ui/icon';
import { Reveal } from '@/components/ui/reveal';
import { Stagger, useScrollFade } from '@/components/ui/motion';
export type AiOnboardingVariant = 'capabilities' | 'first-task' | 'connect-sources' | 'limits-promise';
export interface AiCapability {
id: string;
title: string;
description: string;
icon?: IconName;
/** A sample request */
prompt: string;
/** The sample result, shown when the capability is picked */
result: string;
}
export interface AiStarterTask {
id: string;
label: string;
icon?: IconName;
/** About how long it takes, e.g. "1 min" */
time?: string;
/** What the assistant asks for to start */
needs?: string;
}
export interface AiSource {
id: string;
name: string;
icon?: IconName;
description: string;
/** What the assistant reads */
reads: string;
connected?: boolean;
}
export interface AiPromiseItem {
title: string;
description: string;
icon?: IconName;
}
export interface AiOnboardingProps {
variant?: AiOnboardingVariant;
/** The assistant's name */
name?: string;
title?: string;
description?: string;
capabilities?: AiCapability[];
tasks?: AiStarterTask[];
sources?: AiSource[];
/** limits-promise: what it does well and what it does not */
can?: string[];
cannot?: string[];
promises?: AiPromiseItem[];
/** Step shown in the progress line, e.g. { current: 2, total: 3 } */
step?: { current: number; total: number };
/** Continue, the step is marked done and the next segment fills */
onNext?: () => void;
/** Back and Skip only show when they are handled */
onBack?: () => void;
onSkip?: () => void;
/** first-task: the chosen task or the own words */
onStart?: (task: string) => void;
labels?: Partial<(typeof TEXTS)['en']>;
className?: string;
}
const TEXTS = {
en: {
step: 'Step {n} of {total}',
next: 'Continue',
back: 'Back',
skip: 'Skip for now',
request: 'You ask',
result: 'Result',
start: 'Start with this',
started: 'Your first chat starts with this task.',
stepDone: 'Step {n} done',
needs: 'Needs',
connect: 'Connect',
connected: 'Connected',
reads: 'Reads',
later: 'You can connect more later in settings.',
count: '{n} of {total} connected',
can: 'Good at',
cannot: 'Not made for',
promise: 'Our promise',
agree: 'Understood, start now',
custom: 'Or describe your own task',
customPlaceholder: 'Plan the team day in November',
},
de: {
step: 'Schritt {n} von {total}',
next: 'Weiter',
back: 'Zurück',
skip: 'Später',
request: 'Du fragst',
result: 'Ergebnis',
start: 'Damit starten',
started: 'Dein erster Chat startet mit dieser Aufgabe.',
stepDone: 'Schritt {n} erledigt',
needs: 'Braucht',
connect: 'Verbinden',
connected: 'Verbunden',
reads: 'Liest',
later: 'Weitere Quellen kannst du später in den Einstellungen verbinden.',
count: '{n} von {total} verbunden',
can: 'Gut darin',
cannot: 'Nicht gemacht für',
promise: 'Unser Versprechen',
agree: 'Verstanden, jetzt starten',
custom: 'Oder beschreib deine eigene Aufgabe',
customPlaceholder: 'Plane den Teamtag im November',
},
};
type Texts = (typeof TEXTS)['en'];
type P = AiOnboardingProps & { t: Texts };
const fill = (s: string, v: Record<string, string | number>) => s.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ''));
const CSS =
'@keyframes aob-up{from{opacity:0;transform:translateY(8px)}}@keyframes aob-swap{from{opacity:0;transform:translateY(6px);filter:blur(3px)}}@keyframes aob-check{from{transform:scale(0) rotate(-30deg)}}' +
'@media (prefers-reduced-motion:no-preference){.aob-up{animation:aob-up .45s var(--ease-kit) var(--aob-d,0ms) both}.aob-swap{animation:aob-swap .5s var(--ease-kit) both}.aob-check{animation:aob-check .4s var(--ease-kit) both}}';
function Style() {
return (
<style href="kit-ai-onboarding" precedence="default">
{CSS}
</style>
);
}
/* ------------------------------------------------------------------ */
/* Shared parts */
/* ------------------------------------------------------------------ */
/** The onboarding window: progress line, heading, body and footer */
function Shell({ t, name, title, description, step, done, children, footer, className, wide }: { t: Texts; name?: string; title?: string; description?: string; step?: { current: number; total: number }; done?: boolean; children: React.ReactNode; footer?: React.ReactNode; className?: string; wide?: boolean }) {
const filled = step ? Math.min(step.total, step.current + (done ? 1 : 0)) : 0;
return (
<section className={cn('kit-glow kit-dots relative overflow-clip bg-surface py-12 sm:py-16 lg:py-20', className)}>
<div className="kit-container">
<Reveal effect="scale">
<div className={cn('mx-auto w-full overflow-hidden rounded-[24px] border border-border bg-card shadow-[var(--highlight),var(--shadow-lg)]', wide ? 'max-w-[60rem]' : 'max-w-[44rem]')}>
{step && (
<div className="flex gap-1.5 px-5 pt-5 sm:px-8 sm:pt-7" role="progressbar" aria-valuemin={1} aria-valuemax={step.total} aria-valuenow={filled} aria-label={fill(t.step, { n: step.current, total: step.total })}>
{Array.from({ length: step.total }, (_, i) => (
<span key={i} className="h-1 flex-1 overflow-hidden rounded-full bg-[color-mix(in_oklab,var(--foreground)_10%,transparent)]">
<span className={cn('block h-full origin-left rounded-full bg-primary transition-transform duration-700 ease-[var(--ease-kit)]', i < filled ? 'scale-x-100' : 'scale-x-0')} style={{ transitionDelay: `${i * 120}ms` }} />
</span>
))}
</div>
)}
<div className="px-5 pb-2 pt-6 sm:px-8 sm:pt-7">
<p className="flex items-center gap-2 text-[13px] font-medium text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]">
<span aria-hidden className="grid size-6 place-items-center rounded-full bg-primary text-primary-foreground">
<Icon name="sparkle" size={13} />
</span>
{name}
{step && <span className="text-subtle-foreground">· {fill(t.step, { n: step.current, total: step.total })}</span>}
</p>
{title && <h2 className="mt-3 text-balance text-[24px] font-semibold leading-tight tracking-[-0.02em] text-foreground sm:text-[30px]">{title}</h2>}
{description && <p className="mt-2 max-w-[36rem] text-pretty text-[15px] leading-relaxed text-muted-foreground">{description}</p>}
</div>
<div className="px-5 py-5 sm:px-8">{children}</div>
{footer && <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-surface/60 px-5 py-4 sm:px-8 [&>:only-child]:ml-auto">{footer}</div>}
</div>
</Reveal>
</div>
</section>
);
}
const primaryBtn = 'inline-flex h-11 items-center justify-center gap-1.5 rounded-lg bg-primary px-5 text-[14px] max-sm:w-full 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-10';
const ghostBtn = 'inline-flex h-11 items-center justify-center rounded-lg px-3 text-[14px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground sm:h-10';
function Footer({ t, children, onBack, onSkip }: { t: Texts; children?: React.ReactNode; onBack?: () => void; onSkip?: () => void }) {
return (
<>
{(onBack || onSkip) && (
<span className="flex gap-1">
{onBack && (
<button type="button" onClick={onBack} className={ghostBtn}>
<Icon name="arrow-left" size={15} className="sm:mr-1" />
<span className="max-sm:sr-only">{t.back}</span>
</button>
)}
{onSkip && (
<button type="button" onClick={onSkip} className={ghostBtn}>
{t.skip}
</button>
)}
</span>
)}
{children}
</>
);
}
/** Continue, then the step reads as done */
function Next({ t, done, step, onClick, label }: { t: Texts; done: boolean; step?: { current: number; total: number }; onClick: () => void; label?: string }) {
return done ? (
<span role="status" className="aob-up inline-flex h-11 items-center gap-2 text-[14px] font-medium text-success-foreground sm:h-10">
<Icon name="check-circle" size={16} />
{fill(t.stepDone, { n: step?.current ?? 1 })}
</span>
) : (
<button type="button" onClick={onClick} className={primaryBtn}>
{label ?? t.next}
<Icon name="arrow-right" size={15} />
</button>
);
}
/* ------------------------------------------------------------------ */
/* capabilities */
/* ------------------------------------------------------------------ */
function Capabilities({ t, name, title, description, capabilities = [], step, onNext, onBack, onSkip, className }: P) {
const [sel, setSel] = useState(0);
const [typed, setTyped] = useState('');
const [next, setNext] = useState(false);
const [ref, inView] = useInView<HTMLDivElement>();
// Types once when it first comes into view, not again on every return
const [seen, setSeen] = useState(false);
useEffect(() => {
if (inView) setSeen(true);
}, [inView]);
const cur = capabilities[sel];
const refs = useRef<(HTMLButtonElement | null)[]>([]);
const [row, fade] = useScrollFade<HTMLDivElement>();
// The example request types itself, then the result appears
useEffect(() => {
if (!cur) return;
if (!seen || prefersReducedMotion()) return setTyped(cur.prompt);
setTyped('');
let i = 0;
const id = window.setInterval(() => {
i += 2;
setTyped(cur.prompt.slice(0, i));
if (i >= cur.prompt.length) window.clearInterval(id);
}, 22);
return () => window.clearInterval(id);
}, [cur, seen]);
const done = typed.length >= (cur?.prompt.length ?? 0);
return (
<Shell t={t} name={name} title={title} description={description} step={step} done={next} className={className} wide footer={<Footer t={t} onSkip={onSkip} onBack={onBack}><Next t={t} done={next} step={step} onClick={() => (onNext?.(), setNext(true))} /></Footer>}>
<div ref={ref} className="grid grid-cols-1 gap-4 md:grid-cols-[minmax(0,17rem)_minmax(0,1fr)] md:gap-6">
<div ref={row} role="tablist" aria-orientation="vertical" aria-label={title} className={cn('kit-no-scrollbar -mx-5 flex gap-2 overflow-x-auto px-5 md:mx-0 md:flex-col md:overflow-visible md:px-0 md:[mask-image:none]', fade)}>
{capabilities.map((c, i) => {
const on = i === sel;
return (
<button
key={c.id}
ref={(el) => {
refs.current[i] = el;
}}
type="button"
role="tab"
aria-selected={on}
tabIndex={on ? 0 : -1}
onClick={() => setSel(i)}
onKeyDown={(e) => {
const d = ['ArrowDown', 'ArrowRight'].includes(e.key) ? 1 : ['ArrowUp', 'ArrowLeft'].includes(e.key) ? -1 : 0;
if (!d) return;
e.preventDefault();
const n = (i + d + capabilities.length) % capabilities.length;
setSel(n);
refs.current[n]?.focus();
}}
className={cn('flex w-[15rem] shrink-0 items-start gap-3 rounded-xl border p-3 text-left transition-[border-color,background-color] duration-300 md:w-full', on ? 'border-[color-mix(in_oklab,var(--primary)_50%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_7%,var(--card))]' : 'border-border hover:bg-accent/60')}
>
<span aria-hidden className={cn('grid size-9 shrink-0 place-items-center rounded-lg transition-colors', on ? 'bg-primary text-primary-foreground' : 'bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] text-muted-foreground')}>
<Icon name={c.icon ?? 'sparkle'} size={17} />
</span>
<span className="min-w-0">
<span className="block text-[14px] font-medium text-foreground">{c.title}</span>
<span className="mt-0.5 block text-[12.5px] leading-snug text-muted-foreground">{c.description}</span>
</span>
</button>
);
})}
</div>
{cur && (
<div role="tabpanel" className="rounded-2xl border border-border bg-background/60 p-4 sm:p-5" aria-live="polite">
<p className="text-[12px] font-medium text-subtle-foreground">{t.request}</p>
<p className="mt-1.5 min-h-[3.25rem] rounded-xl bg-[color-mix(in_oklab,var(--primary)_10%,var(--card))] px-3.5 py-2.5 text-[14.5px] leading-relaxed text-foreground">
{typed}
{!done && <span aria-hidden className="ml-0.5 inline-block h-[1em] w-0.5 translate-y-[0.15em] animate-kit-pulse bg-primary" />}
</p>
<p className="mt-4 text-[12px] font-medium text-subtle-foreground">{t.result}</p>
<div className="relative mt-1.5">
<p aria-hidden className="invisible whitespace-pre-line text-[14.5px] leading-relaxed">{cur.result}</p>
<p key={`${sel}-${done}`} className={cn('absolute inset-0 whitespace-pre-line text-pretty text-[14.5px] leading-relaxed text-foreground', done ? 'aob-swap' : 'opacity-0')}>
{cur.result}
</p>
</div>
</div>
)}
</div>
</Shell>
);
}
/* ------------------------------------------------------------------ */
/* first-task */
/* ------------------------------------------------------------------ */
function FirstTask({ t, name, title, description, tasks = [], step, onBack, onSkip, onStart, className }: P) {
const [pick, setPick] = useState<string | null>(null);
const [own, setOwn] = useState('');
const [go, setGo] = useState(false);
const chosen = tasks.find((x) => x.id === pick);
const refs = useRef<(HTMLButtonElement | null)[]>([]);
return (
<Shell
t={t}
name={name}
title={title}
description={description}
step={step}
done={go}
className={className}
footer={
<Footer t={t} onBack={onBack} onSkip={onSkip}>
{go ? (
<span role="status" className="aob-up inline-flex items-center gap-2 text-[14px] font-medium text-success-foreground">
<Icon name="check-circle" size={16} />
{t.started}
</span>
) : (
<button type="button" disabled={!pick && !own.trim()} onClick={() => (onStart?.(chosen?.label ?? own.trim()), setGo(true))} className={primaryBtn}>
{t.start}
<Icon name="arrow-right" size={15} />
</button>
)}
</Footer>
}
>
<Stagger role="radiogroup" aria-label={title} step={70} className="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
{tasks.map((x, i) => {
const on = pick === x.id;
return (
<button
key={x.id}
ref={(el) => {
refs.current[i] = el;
}}
type="button"
role="radio"
aria-checked={on}
tabIndex={on || (!pick && i === 0) ? 0 : -1}
onClick={() => (setPick(x.id), setOwn(''), setGo(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 + tasks.length) % tasks.length;
setPick(tasks[n].id);
setOwn('');
setGo(false);
refs.current[n]?.focus();
}}
className={cn('group flex h-full items-start gap-3 rounded-xl border p-3.5 text-left transition-[border-color,background-color,transform] duration-300 ease-[var(--ease-kit)]', on ? 'border-[color-mix(in_oklab,var(--primary)_55%,var(--border))] bg-[color-mix(in_oklab,var(--primary)_7%,var(--card))]' : 'border-border hover:-translate-y-0.5 hover:border-border-strong motion-reduce:hover:translate-y-0')}>
<span aria-hidden className={cn('grid size-9 shrink-0 place-items-center rounded-lg transition-colors', on ? 'bg-primary text-primary-foreground' : 'bg-[color-mix(in_oklab,var(--foreground)_6%,transparent)] text-muted-foreground')}>
<Icon name={on ? 'check' : x.icon ?? 'sparkle'} size={17} className={on ? 'aob-check' : undefined} />
</span>
<span className="min-w-0 flex-1">
<span className="block text-[14px] font-medium leading-snug text-foreground">{x.label}</span>
<span className="mt-1 flex flex-wrap gap-x-3 text-[12px] text-muted-foreground">
{x.time && (
<span className="inline-flex items-center gap-1">
<Icon name="clock" size={12} />
{x.time}
</span>
)}
{x.needs && (
<span>{`${t.needs} ${x.needs}`}</span>
)}
</span>
</span>
</button>
);
})}
</Stagger>
<label className="mt-4 block">
<span className="mb-1.5 block text-[13px] font-medium text-foreground">{t.custom}</span>
<input value={own} onChange={(e) => (setOwn(e.target.value), setPick(null), setGo(false))} placeholder={t.customPlaceholder} className="h-11 w-full rounded-xl border border-border bg-background px-3.5 text-[14.5px] text-foreground outline-none placeholder:text-subtle-foreground focus:border-[color-mix(in_oklab,var(--primary)_55%,var(--border))] focus:shadow-[0_0_0_3px_color-mix(in_oklab,var(--primary)_16%,transparent)] focus-visible:outline-none" />
</label>
{chosen && <span className="sr-only" aria-live="polite">{chosen.label}</span>}
</Shell>
);
}
/* ------------------------------------------------------------------ */
/* connect-sources */
/* ------------------------------------------------------------------ */
function ConnectSources({ t, name, title, description, sources = [], step, onNext, onBack, onSkip, className }: P) {
const [on, setOn] = useState<Record<string, boolean>>(() => Object.fromEntries(sources.map((s) => [s.id, !!s.connected])));
const [busy, setBusy] = useState<string | null>(null);
const [next, setNext] = useState(false);
const n = sources.filter((s) => on[s.id]).length;
const connect = (id: string) => {
setBusy(id);
window.setTimeout(() => (setOn((o) => ({ ...o, [id]: true })), setBusy(null)), prefersReducedMotion() ? 0 : 900);
};
return (
<Shell
t={t}
name={name}
title={title}
description={description}
step={step}
done={next}
className={className}
footer={
<Footer t={t} onBack={onBack} onSkip={onSkip}>
<Next t={t} done={next} step={step} onClick={() => (onNext?.(), setNext(true))} />
</Footer>
}
>
<Stagger as="ul" step={70} className="space-y-2.5">
{sources.map((s) => {
const c = on[s.id];
return (
<li key={s.id} className={cn('flex flex-col gap-3 rounded-xl border p-3.5 transition-[border-color,background-color] duration-500 sm:flex-row sm:items-center', c ? 'border-[color-mix(in_oklab,var(--success)_40%,var(--border))] bg-[color-mix(in_oklab,var(--success)_5%,var(--card))]' : 'border-border')}>
<div className="flex min-w-0 flex-1 items-start gap-3">
<span aria-hidden className="grid size-10 shrink-0 place-items-center rounded-xl border border-border bg-background text-foreground">
<Icon name={s.icon ?? 'plug'} size={19} />
</span>
<div className="min-w-0">
<p className="text-[14.5px] font-medium text-foreground">{s.name}</p>
<p className="text-[13px] leading-snug text-muted-foreground">{s.description}</p>
<p className="mt-1 flex items-start gap-1 text-[12px] text-subtle-foreground">
<Icon name="eye" size={12} className="mt-0.5 shrink-0" />
{`${t.reads} ${s.reads}`}
</p>
</div>
</div>
{c ? (
<span className="inline-flex h-11 shrink-0 items-center gap-1.5 self-start px-1 text-[13.5px] font-medium text-success-foreground sm:h-9 sm:self-auto">
<span className="aob-check grid size-5 place-items-center rounded-full bg-[color-mix(in_oklab,var(--success)_18%,var(--card))] text-success-foreground">
<Icon name="check" size={12} strokeWidth={2.6} />
</span>
{t.connected}
</span>
) : (
<button type="button" disabled={busy === s.id} onClick={() => connect(s.id)} className="inline-flex h-11 shrink-0 items-center gap-1.5 self-start rounded-lg border border-border bg-card px-4 text-[13.5px] font-medium text-foreground hover:bg-accent disabled:opacity-70 sm:h-9 sm:self-auto">
<Icon name={busy === s.id ? 'loader' : 'plug'} size={15} className={busy === s.id ? 'animate-spin' : undefined} />
{t.connect}
</button>
)}
</li>
);
})}
</Stagger>
<div className="mt-4 flex flex-col gap-1.5 sm:flex-row sm:items-start sm:justify-between sm:gap-6">
<p className="flex items-start gap-2 text-[12.5px] text-muted-foreground">
<Icon name="info" size={14} className="mt-px shrink-0" />
{t.later}
</p>
<p className="shrink-0 text-[12.5px] font-medium tabular-nums text-foreground" aria-live="polite">
{fill(t.count, { n, total: sources.length })}
</p>
</div>
</Shell>
);
}
/* ------------------------------------------------------------------ */
/* limits-promise */
/* ------------------------------------------------------------------ */
function LimitsPromise({ t, name, title, description, can = [], cannot = [], promises = [], step, onNext, onBack, onSkip, className }: P) {
const [ok, setOk] = useState(false);
return (
<Shell
t={t}
name={name}
title={title}
description={description}
step={step}
done={ok}
className={className}
wide
footer={
<Footer t={t} onBack={onBack} onSkip={onSkip}>
<Next t={t} done={ok} step={step} label={t.agree} onClick={() => (onNext?.(), setOk(true))} />
</Footer>
}
>
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
{[
{ label: t.can, items: can, icon: 'check' as IconName, tone: 'success' },
{ label: t.cannot, items: cannot, icon: 'minus' as IconName, tone: 'muted' },
].map((col, ci) => (
<Reveal key={col.label} delay={ci * 120} className="rounded-2xl border border-border bg-background/60 p-4 sm:p-5">
<p className="text-[13px] font-semibold text-foreground">{col.label}</p>
<ul className="mt-3 space-y-2.5">
{col.items.map((x, i) => (
<li key={x} className="aob-up flex items-start gap-2.5 text-[14px] leading-snug text-foreground" style={{ '--aob-d': `${200 + ci * 120 + i * 70}ms` } as React.CSSProperties}>
<span aria-hidden className={cn('mt-0.5 grid size-5 shrink-0 place-items-center rounded-full', col.tone === 'success' ? 'bg-[color-mix(in_oklab,var(--success)_16%,var(--card))] text-success-foreground' : 'bg-[color-mix(in_oklab,var(--foreground)_8%,transparent)] text-muted-foreground')}>
<Icon name={col.icon} size={12} strokeWidth={2.4} />
</span>
{x}
</li>
))}
</ul>
</Reveal>
))}
</div>
{promises.length > 0 && (
<div className="mt-5">
<p className="mb-3 text-[13px] font-semibold text-foreground">{t.promise}</p>
<Stagger step={90} className="grid grid-cols-1 gap-px overflow-hidden rounded-2xl border border-border bg-border sm:grid-cols-3">
{promises.map((p) => (
<div key={p.title} className="bg-card p-4">
<Icon name={p.icon ?? 'shield'} size={18} className="text-primary" />
<p className="mt-2.5 text-[14px] font-medium text-foreground">{p.title}</p>
<p className="mt-1 text-pretty text-[13px] leading-relaxed text-muted-foreground">{p.description}</p>
</div>
))}
</Stagger>
</div>
)}
</Shell>
);
}
/* ------------------------------------------------------------------ */
/**
* Onboarding for an AI assistant: what it can do with an example that types
* itself, a first task to start with, the data sources to connect with what
* each one reads, and an honest page on limits and promises.
*/
export function AiOnboarding({ variant = 'capabilities', labels, ...props }: AiOnboardingProps) {
const t = useTexts(TEXTS, labels);
const p: P = { ...props, t };
const key = JSON.stringify([props.capabilities, props.tasks, props.sources]);
const body = (() => {
switch (variant) {
case 'first-task':
return <FirstTask key={key} {...p} />;
case 'connect-sources':
return <ConnectSources key={key} {...p} />;
case 'limits-promise':
return <LimitsPromise {...p} />;
default:
return <Capabilities key={key} {...p} />;
}
})();
return (
<>
<Style />
{body}
</>
);
}Häufige Fragen
Wie installiere ich KI-Einstieg?
Mit aiw add @ai-website/ai-onboarding. Der Befehl legt die Datei components/ui/ai-onboarding.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind icon, reveal und motion. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat KI-Einstieg?
4 Varianten, Was er kann, Erste Aufgabe, Quellen verbinden und Grenzen und Versprechen. Du wählst sie mit der Prop variant, zum Beispiel variant="first-task".
Kann ich die Texte von KI-Einstieg übersetzen?
Ja. KI-Einstieg bringt 21 eigene Texte auf Deutsch und Englisch mit, etwa „Weiter“, „Zurück“ und „Später“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent KI-Einstieg einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini KI-Einstieg mit aiw_components und installieren sie mit aiw_add @ai-website/ai-onboarding. Nenn im Prompt die Variante, etwa variant="capabilities", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich KI-Einstieg ohne Code anpassen?
Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden AiOnboarding-Code zum Kopieren.