import type { ExampleUsage, StoryUsage } from './types'; /** Basic Input — a text box with a send button that enables once you type. */ const basic: StoryUsage = { intro: 'A complete prompt box from one element. `` renders the textarea and send button; bind `value`, handle `valuechange` on every keystroke, and `submit` (Enter or the send button) gives you `{ value, attachments }`. (The live demo composes the SolidJS `PromptInput` primitives.)', snippets: { html: ` `, react: `import { useState } from 'react'; import { PromptInput } from '@kitn.ai/chat/react'; export function Prompt() { const [value, setValue] = useState(''); return ( setValue(e.detail.value)} onSubmit={(e) => { const { value, attachments } = e.detail; console.log(value, attachments); }} /> ); }`, vue: ` `, svelte: ` `, angular: `// main.ts: import '@kitn.ai/chat/elements' before bootstrapApplication, // and add CUSTOM_ELEMENTS_SCHEMA to the component/module. import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class PromptComponent { value = ''; onValueChange(e: CustomEvent<{ value: string }>) { this.value = e.detail.value; // track on every keystroke } onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) { const { value, attachments } = e.detail; console.log(value, attachments); } }`, solid: `import { createSignal } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions, Button } from '@kitn.ai/chat'; import { ArrowUp } from 'lucide-solid'; export function Prompt() { const [value, setValue] = createSignal(''); return ( setValue('')}> ); }`, }, }; /** With Suggestion Chips — starter prompts above the input. */ const suggestions: StoryUsage = { intro: 'Show starter prompts above the input. Pass a `suggestions` array (as a PROPERTY) and pick `suggestionMode` — `"submit"` (default) sends the prompt immediately, `"fill"` just drops it into the box and fires `kc-suggestion-click`. (The demo groups its chips with the SolidJS `PromptSuggestion` primitive, which the element renders as one flat row.)', snippets: { html: ` `, react: `import { PromptInput } from '@kitn.ai/chat/react'; const SUGGESTIONS = [ 'Summarize this document', 'What are the key takeaways?', 'Create an outline', ]; export function Prompt() { return ( console.log(e.detail.value)} onSubmit={(e) => console.log(e.detail.value)} /> ); }`, vue: ` `, svelte: ` `, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class PromptComponent { // [suggestions] binds the array as a property. suggestions = [ 'Summarize this document', 'What are the key takeaways?', 'Create an outline', ]; onSuggestionClick(e: CustomEvent<{ value: string }>) { console.log(e.detail.value); } onSubmit(e: CustomEvent<{ value: string }>) { console.log(e.detail.value); } }`, solid: `import { createSignal, For } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions, PromptSuggestion, Button } from '@kitn.ai/chat'; import { ArrowUp } from 'lucide-solid'; const GROUPS = [ { label: 'Get started', items: ['Summarize this document', 'What are the key takeaways?', 'Create an outline'] }, { label: 'Go deeper', items: ['Compare with similar approaches', 'What are the tradeoffs?', 'Find contradictions'] }, ]; export function Prompt() { const [value, setValue] = createSignal(''); return (
{(group) => (
{group.label}
{(item) => setValue(item)}>{item}}
)}
setValue('')}>
); }`, }, }; /** With Action Buttons — toolbar buttons beside the input. */ const actionButtons: StoryUsage = { intro: 'Add toolbar buttons beside the input. `` has built-in Search and Voice buttons — enable `search` and `voice`, then handle the `search` / `voice` events; attaching files is built in (the paperclip, emitted on `submit` as `attachments`). For extra custom buttons, place `` children inside `` — the element reads them as invisible data carriers and renders a ghost icon button per entry in the left toolbar; clicking fires a `kc-toolbar-action` CustomEvent with `detail.action` = the action id. This is the same `` descriptor element that `` uses (composition symmetry). The Solid tab shows a custom Sparkles button composed directly with the `PromptInput` primitives (the full-control equivalent).', snippets: { html: ` `, react: `import { PromptInput } from '@kitn.ai/chat/react'; export function Prompt() { return ( console.log('search clicked')} onVoice={() => console.log('voice clicked')} onSubmit={(e) => { const { value, attachments } = e.detail; // attachments from the paperclip console.log(value, attachments); }} /> ); }`, vue: ` `, svelte: ` `, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class PromptComponent { onSearch() { console.log('search clicked'); } onVoice() { console.log('voice clicked'); } onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) { const { value, attachments } = e.detail; // attachments from the paperclip console.log(value, attachments); } }`, solid: `import { createSignal } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions, Button } from '@kitn.ai/chat'; import { ArrowUp, Paperclip, Globe, Mic, Sparkles } from 'lucide-solid'; export function Prompt() { const [value, setValue] = createSignal(''); return ( setValue('')}>
{/* Sparkles: a custom button composed directly. For the element, use instead. */}
); }`, }, }; /** Streaming / Loading State — disabled while a reply streams in. */ const streaming: StoryUsage = { intro: 'Block input while a reply streams. Set `loading` to show the streaming state and stop accepting submits, and `disabled` to make the box fully non-interactive. Add `stoppable` to get a built-in Stop button that fires `kc-stop` — listen for that event and call `controller.abort()` on your fetch/SSE. (The demo composes the SolidJS `PromptInput` + `Loader` primitives to show the typing/dots indicators and a stop button.)', snippets: { html: ` `, react: `import { PromptInput } from '@kitn.ai/chat/react'; export function Prompt({ isStreaming }: { isStreaming: boolean }) { return ( ); }`, vue: ` `, svelte: ` `, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class PromptComponent { isStreaming = true; }`, solid: `import { PromptInput, PromptInputTextarea, PromptInputActions, Loader, Button } from '@kitn.ai/chat'; import { Square } from 'lucide-solid'; export function Prompt() { return (
Generating...
); }`, }, }; /** With Model Selector — a model picker alongside the input. * * INSIGHT: `ModelSwitcher` only renders when `models.length > 1`. Passing a * single-item array hides it entirely — so the render path for free/pro tiers * (single model) is already handled without conditional code. */ const modelSelector: StoryUsage = { intro: 'Put a model picker beside the input. `` doesn\'t expose a model-switcher prop — pair it with the standalone `` element (bind `models` and `currentModel`, handle `modelchange`) and lay them out side by side. (The demo composes the SolidJS `PromptInput` + `ModelSwitcher` primitives in the actions row.)', snippets: { html: `
`, react: `import { useState } from 'react'; import { PromptInput, ModelSwitcher } from '@kitn.ai/chat/react'; const MODELS = [ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' }, { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' }, ]; export function Prompt() { const [modelId, setModelId] = useState('claude-4'); return (
setModelId(e.detail.modelId)} /> console.log(e.detail.value)} />
); }`, vue: ` `, svelte: `
console.log(e.detail.value)} />
`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \`
\`, }) export class PromptComponent { modelId = 'claude-4'; // [models] binds the array as a property. models = [ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' }, { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' }, ]; onModelChange(e: CustomEvent<{ modelId: string }>) { this.modelId = e.detail.modelId; } onSubmit(e: CustomEvent<{ value: string }>) { console.log(e.detail.value); } }`, solid: `import { createSignal } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions, ModelSwitcher, Button } from '@kitn.ai/chat'; import type { ModelOption } from '@kitn.ai/chat'; import { ArrowUp, Paperclip } from 'lucide-solid'; const MODELS: ModelOption[] = [ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' }, { id: 'gemini-2', name: 'Gemini 2.5 Pro', provider: 'Google' }, ]; export function Prompt() { const [value, setValue] = createSignal(''); const [modelId, setModelId] = createSignal('claude-4'); return ( setValue('')}>
); }`, }, }; /** With File Attachments — staged files rendered above the textarea. */ const withFileAttachments: StoryUsage = { intro: 'The `kc-prompt-input` element has a built-in paperclip: clicking it opens a file picker, previews appear above the textarea (removable chips), and `kc-submit` always carries `{ value, attachments: AttachmentData[] }` — even when the array is empty. To pre-populate staged files, set `prompt.attachments = [...]` as a JS **property** after mount; the element then manages its own attachment state from there. The Solid demo wires the `Attachments`/`Attachment`/`AttachmentPreview`/`AttachmentInfo`/`AttachmentRemove` primitives manually for full control — use the element if you want the paperclip UX for free.', snippets: { html: ` `, react: `import { useRef } from 'react'; import { PromptInput } from '@kitn.ai/chat/react'; const SEED = [ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' }, { id: 'a2', type: 'file', filename: 'screenshot.png', mediaType: 'image/png' }, ]; export function Prompt() { const ref = useRef(null); // Set the attachments property after mount. // useEffect(() => { if (ref.current) ref.current.attachments = SEED; }, []); return ( { // attachments is always AttachmentData[] — may be empty const { value, attachments } = e.detail; console.log(value, attachments); }} /> ); }`, vue: ` `, svelte: ` `, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA, AfterViewInit, ViewChild, ElementRef } from '@angular/core'; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class PromptComponent implements AfterViewInit { @ViewChild('prompt') promptRef!: ElementRef; // Set attachments as a property after mount. ngAfterViewInit() { this.promptRef.nativeElement.attachments = [ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' }, ]; } onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) { const { value, attachments } = e.detail; // attachments always present console.log(value, attachments); } }`, solid: `import { createSignal, For, Show } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions, Button, Attachments, Attachment, AttachmentPreview, AttachmentInfo, AttachmentRemove, } from '@kitn.ai/chat'; import type { AttachmentData } from '@kitn.ai/chat'; import { ArrowUp, Paperclip } from 'lucide-solid'; // The Solid PromptInput primitives don't wire the paperclip for you — that's // done by DefaultPromptInput (used internally by the kc-prompt-input element). // Compose the Attachments primitives manually when you need full control. export function Prompt() { const [value, setValue] = createSignal(''); const [attachments, setAttachments] = createSignal([ { id: 'a1', type: 'file', filename: 'architecture.pdf', mediaType: 'application/pdf' }, { id: 'a2', type: 'file', filename: 'screenshot.png', mediaType: 'image/png' }, ]); let fileInput: HTMLInputElement | undefined; const addFiles = (files: FileList | null) => { if (!files?.length) return; setAttachments((prev) => [ ...prev, ...Array.from(files).map((f) => ({ id: crypto.randomUUID(), type: 'file' as const, filename: f.name, mediaType: f.type || undefined, url: f.type.startsWith('image/') ? URL.createObjectURL(f) : undefined, })), ]); }; const removeAttachment = (id: string) => setAttachments((prev) => prev.filter((a) => a.id !== id)); const handleSubmit = () => { // kc-submit emits { value, attachments } — mirror that shape here console.log('submit', { value: value(), attachments: attachments() }); setValue(''); setAttachments([]); }; return ( <> { addFiles(e.currentTarget.files); e.currentTarget.value = ''; }} /> 0}>
{(att) => ( removeAttachment(att.id)}> )}
{/* send enabled when there's text OR staged attachments */}
); }`, }, }; /** Full Example — everything combined: model switcher, grouped suggestions, * streaming state with a Stop button, and a send button that enables on input. * * GOTCHAS compiled from the source: * - Submit payload: `kc-submit` always emits `{ value: string; attachments: AttachmentData[] }`. * Even when no files are staged, `attachments` is an empty array — never `undefined`. * - Enter vs Shift+Enter: `PromptInputTextarea` intercepts `Enter` (no Shift) and calls * `onSubmit`; `Shift+Enter` inserts a newline. This is wired at the primitive level * (`handleKeyDown` in prompt-input.tsx line 147), not configurable. * - `loading` vs `disabled` on the element: `loading` alone blocks submit but keeps the * box visually interactive; `disabled` makes it fully non-interactive (opacity + no focus). * Use both together for the streaming state. * - `isLoading` on the Solid primitive vs `loading` on the element: the Solid * `PromptInput` prop is `isLoading`; the web component attribute is `loading` (kebab). * - Stop button: add `stoppable` to `kc-prompt-input` and listen for `kc-stop` — the element * fires it when the Stop button is clicked. Call `controller.abort()` in your handler to * cancel the fetch/SSE. When composing Solid primitives, wire the Square button yourself * (see FullExample). The element does the toggling for you; the consumer still owns the abort. * - `ModelSwitcher` only renders when `models.length > 1` — a single-model list hides it. * - `suggestions` is a JS property, not an attribute — arrays must be set on the element * reference (not via an HTML attribute string). In the web-component tab note the * `.prop` binding for Vue, `$:` for Svelte, `[prop]` for Angular. * - `suggestionMode="submit"` (default) immediately dispatches `kc-submit` when a chip * is clicked; `suggestionMode="fill"` drops the text into the box and fires * `kc-suggestion-click` instead (so the user can edit before sending). */ const fullExample: StoryUsage = { intro: 'Everything combined: model switcher, grouped suggestion chips, streaming state (with a Stop button), and a send button that enables once you type. Simulates the idle → streaming → idle loop you\'d wire to a real fetch/SSE call. Key gotchas: `kc-submit` always emits `{ value, attachments }` (attachments may be empty); Enter submits, Shift+Enter newlines; `loading` blocks submit while `disabled` kills focus too — use both while streaming; add `stoppable` to enable the built-in Stop button — it fires `kc-stop` when clicked; call `controller.abort()` in your handler to cancel the stream. When composing Solid primitives (the `PromptInput` + `PromptInputActions` pattern), wire the Square button yourself as shown in the Full Example story.', snippets: { html: `
`, react: `import { useState, useRef } from 'react'; import { PromptInput, ModelSwitcher } from '@kitn.ai/chat/react'; const MODELS = [ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' }, ]; const SUGGESTIONS = ['Summarize this document', 'What are the key takeaways?']; export function Prompt() { const [modelId, setModelId] = useState('claude-4-opus'); const [streaming, setStreaming] = useState(false); const controllerRef = useRef(null); const handleSubmit = async (e: CustomEvent<{ value: string; attachments: unknown[] }>) => { // attachments always present, may be empty array const { value, attachments } = e.detail; setStreaming(true); controllerRef.current = new AbortController(); try { await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ value, modelId, attachments }), signal: controllerRef.current.signal, }); } catch (err: unknown) { if ((err as Error).name !== 'AbortError') throw err; } finally { setStreaming(false); } }; return (
setModelId(e.detail.modelId)} /> {/* suggestions passed as array property; suggestionMode="fill" lets user edit first */} {streaming && ( {/* With stoppable + onStop, kc-prompt-input renders the Stop button for you. Here we compose it manually for illustration. */} )}
); }`, vue: ` `, svelte: `
{#if streaming} {/if}
`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA, signal, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; const MODELS = [ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' }, ]; const SUGGESTIONS = ['Summarize this document', 'What are the key takeaways?']; @Component({ selector: 'app-prompt', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \`
@if (streaming()) { }
\`, }) export class PromptComponent { MODELS = MODELS; SUGGESTIONS = SUGGESTIONS; modelId = signal('claude-4-opus'); streaming = signal(false); private controller: AbortController | null = null; onModelChange(e: CustomEvent<{ modelId: string }>) { this.modelId.set(e.detail.modelId); } async onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) { const { value, attachments } = e.detail; // attachments always present this.streaming.set(true); this.controller = new AbortController(); try { await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ value, modelId: this.modelId(), attachments }), signal: this.controller.signal, }); } catch (err: unknown) { if ((err as Error).name !== 'AbortError') throw err; } finally { this.streaming.set(false); } } stop() { this.controller?.abort(); } }`, solid: `import { createSignal, For, Show } from 'solid-js'; import { PromptInput, PromptInputTextarea, PromptInputActions, PromptSuggestion, ModelSwitcher, Loader, Button } from '@kitn.ai/chat'; import type { ModelOption } from '@kitn.ai/chat'; import { ArrowUp, Square } from 'lucide-solid'; const MODELS: ModelOption[] = [ { id: 'claude-4-opus', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'claude-4-sonnet', name: 'Claude 4 Sonnet', provider: 'Anthropic' }, ]; const SUGGESTION_GROUPS = [ { label: 'Get started', items: ['Summarize this document', 'What are the key takeaways?'] }, { label: 'Go deeper', items: ['Compare with similar approaches', 'Find contradictions'] }, ]; export function Prompt() { const [value, setValue] = createSignal(''); const [modelId, setModelId] = createSignal('claude-4-opus'); const [streaming, setStreaming] = createSignal(false); let controller: AbortController | undefined; const handleSubmit = async () => { if (!value().trim()) return; setStreaming(true); setValue(''); controller = new AbortController(); try { // Replace with your real SSE / streaming fetch. await fetch('/api/chat', { method: 'POST', body: JSON.stringify({ value: value(), modelId: modelId() }), signal: controller.signal, }); } catch (err: unknown) { if ((err as Error).name !== 'AbortError') throw err; } finally { setStreaming(false); } }; const handleStop = () => controller?.abort(); return (
{(group) => (
{group.label}
{/* suggestionMode="fill" (default "submit") — here we fill manually */} {(item) => setValue(item)}>{item}}
)}
} >
Generating…
} > {/* Solid primitive: wire the Stop button yourself inside PromptInputActions. With the kc-prompt-input element, add stoppable and listen for kc-stop instead. */}
); }`, }, }; /** * Example: Prompt Input Variants — a complete prompt box (text, suggestions, * action buttons, streaming, model selector, file attachments) built from the * `kc-prompt-input` element. Per-story: the Usage tab shows the snippet for * the story you're on; the example-level fields below are the fallback. */ const promptInputVariants: ExampleUsage = { title: 'Examples/Prompt Input Variants', ...basic, // example-level fallback = the headline "Basic Input" stories: { 'Basic Input': basic, 'With Suggestion Chips': suggestions, 'With Action Buttons': actionButtons, 'Streaming / Loading State': streaming, 'With Model Selector': modelSelector, 'With File Attachments': withFileAttachments, 'Full Example': fullExample, }, }; export default promptInputVariants;