import type { ExampleUsage, StoryUsage } from './types'; // Shared streamed reply used across the snippets (trimmed from the demo). const STREAM = 'Server-Sent Events (SSE) are a lightweight alternative to WebSockets for one-way server-to-client streaming. Use SSE when you only need server push.'; /** Typewriter Streaming — reveal the reply character by character. */ const typewriter: StoryUsage = { intro: 'Reveal an assistant reply character by character. Set the `text` property to a string (or an `AsyncIterable` assigned as a JS property — async iterables cannot be HTML attributes) with `mode="typewriter"` and handle `kc-complete` to unlock the input once all characters are displayed. **Cancel gotcha:** there is no built-in `stop()` — abort your own fetch with an `AbortController` and then clear your streaming state. **Replay gotcha:** passing the same string value again does not re-run the animation; unmount and remount the element instead.', snippets: { html: ` `, react: `import { ResponseStream } from '@kitn.ai/chat/react'; export function StreamedReply() { return ( console.log('done streaming')} /> ); }`, vue: ` `, svelte: ` `, angular: `// main.ts: import '@kitn.ai/chat/elements' before bootstrapApplication, // and add CUSTOM_ELEMENTS_SCHEMA to the component. import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-stream', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class StreamComponent { text = '${STREAM}'; onComplete() { console.log('done streaming'); } }`, solid: `import { createSignal, Show } from 'solid-js'; import { Message, MessageAvatar, ResponseStream } from '@kitn.ai/chat'; export function StreamedReply() { const [streaming, setStreaming] = createSignal(true); return (
setStreaming(false)} class="prose dark:prose-invert prose-sm max-w-none" />
); }`, }, }; /** * Waiting for First Token — the "thinking" state before any token arrives. * This story does NOT stream: there's nothing for kc-response-stream to do yet, * so the snippets show the loading primitives that precede a stream. */ const waiting: StoryUsage = { intro: 'Show a placeholder **before the first token arrives** — `` is not involved yet (there is nothing to stream). Use `` for the thinking spinner and `` for the shimmering label. Once the first chunk arrives, swap them out for ``. Use `` in the input bar to signal that tokens are now *flowing* — `dots` means waiting, `typing` means actively generating.', snippets: { html: `
Thinking...
`, react: `import { Loader, TextShimmer } from '@kitn.ai/chat/react';
Thinking...
`, vue: ` `, svelte: `
Thinking...
`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-waiting', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \`
Thinking...
\`, }) export class WaitingComponent {}`, solid: `import { Message, MessageAvatar, Loader, TextShimmer } from '@kitn.ai/chat'; export function Thinking() { return (
Thinking...
); }`, }, }; /** Fade-in Streaming — words fade in instead of appearing char by char. */ const fade: StoryUsage = { intro: 'Reveal the reply word-by-word with staggered CSS fade-ins instead of a typewriter. Set `mode="fade"` on `` and tune `speed` to control the stagger cadence. **Important:** when `text` is a plain string, `kc-complete` / `onComplete` is **never fired** in fade mode — all segments are delivered immediately and CSS handles the reveal with no detectable endpoint. If you need a completion callback in fade mode, pass an `AsyncIterable` as a property instead (the callback fires after the iterator is exhausted).', snippets: { html: ` `, react: `import { ResponseStream } from '@kitn.ai/chat/react'; console.log('done streaming')} />`, vue: ` `, svelte: ` console.log('done streaming')} />`, angular: `import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; @Component({ selector: 'app-stream', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` \`, }) export class StreamComponent { text = '${STREAM}'; log() { console.log('done streaming'); } }`, solid: `import { createSignal, Show } from 'solid-js'; import { Message, MessageAvatar, ResponseStream } from '@kitn.ai/chat'; export function FadeReply() { const [streaming, setStreaming] = createSignal(true); return (
setStreaming(false)} class="prose dark:prose-invert prose-sm max-w-none" />
); }`, }, }; /** Full Streaming Lifecycle — idle → waiting → streaming → complete in one interactive story. */ const fullLifecycle: StoryUsage = { intro: 'All three phases in one interactive story: **waiting** (dots loader + shimmer before first token), **streaming** (typewriter reveal with a stop button), and **complete** (action bar appears; input unlocks). This is the pattern to follow in production. **Phase ownership:** `ResponseStream` / `kc-response-stream` knows nothing about waiting or cancellation — your app owns a `phase` signal and drives the UI from it. **No built-in cancel:** to stop mid-stream, call `abortController.abort()` on your own fetch and then reset your phase state; the element will stop receiving characters but does not reset its display.', snippets: { html: `
Thinking...
`, react: `import { useState, useEffect, useRef } from 'react'; import { ResponseStream, Loader, TextShimmer } from '@kitn.ai/chat/react'; type Phase = 'idle' | 'waiting' | 'streaming' | 'complete'; export function StreamingChat() { const [phase, setPhase] = useState('idle'); const [mounted, setMounted] = useState(false); const controllerRef = useRef(null); const handleSend = () => { controllerRef.current = new AbortController(); setMounted(false); setPhase('waiting'); // Simulate latency before first token setTimeout(() => { setMounted(true); setPhase('streaming'); }, 1200); }; const handleStop = () => { controllerRef.current?.abort(); setMounted(false); setPhase('idle'); }; return (
{phase === 'waiting' && (
Thinking...
)} {mounted && ( setPhase('complete')} /> )} {phase === 'complete' &&
✓ Action bar here
}
); }`, vue: ` `, svelte: ` {#if phase === 'waiting'}
Thinking...
{/if} {#if mounted} {/if} {#if phase === 'complete'}
✓ Action bar here
{/if} `, angular: `import { Component, signal, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; type Phase = 'idle' | 'waiting' | 'streaming' | 'complete'; @Component({ selector: 'app-streaming-chat', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \`
Thinking...
✓ Action bar here
\`, }) export class StreamingChatComponent { reply = '${STREAM}'; phase = signal('idle'); mounted = signal(false); private controller: AbortController | null = null; toggle() { if (this.phase() === 'idle' || this.phase() === 'complete') this.send(); else this.stop(); } send() { this.controller = new AbortController(); this.mounted.set(false); this.phase.set('waiting'); setTimeout(() => { this.mounted.set(true); this.phase.set('streaming'); }, 1200); } stop() { this.controller?.abort(); this.mounted.set(false); this.phase.set('idle'); } onComplete() { this.phase.set('complete'); } }`, solid: `import { createSignal, Show } from 'solid-js'; import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor, Message, MessageAvatar, MessageContent, MessageActions, PromptInput, PromptInputTextarea, PromptInputActions, ResponseStream, Loader, TextShimmer, Button, } from '@kitn.ai/chat'; import { Square, ArrowUp, Copy, RefreshCw } from 'lucide-solid'; type Phase = 'idle' | 'waiting' | 'streaming' | 'complete'; const REPLY = '${STREAM}'; export function StreamingLifecycle() { const [phase, setPhase] = createSignal('idle'); const [showStream, setShowStream] = createSignal(false); let controller: AbortController | undefined; const handleSend = () => { controller = new AbortController(); setShowStream(false); setPhase('waiting'); // Simulate latency; in production replace with a real fetch: // const res = await fetch('/api/chat', { signal: controller.signal }); setTimeout(() => { setShowStream(true); setPhase('streaming'); }, 1200); }; const handleStop = () => { controller?.abort(); // cancel the real fetch setShowStream(false); // unmounts ResponseStream, stopping character reveals setPhase('idle'); }; return (
{/* Phase 1 — Waiting */}
Thinking...
{/* Phase 2+3 — Streaming → Complete */}
setPhase('complete')} class="prose dark:prose-invert prose-sm max-w-none" />
{/* Action bar only after onComplete fires */}
}> {phase() === 'waiting' ? 'Waiting for response...' : 'Streaming response...'}
} >
); }`, }, }; /** * Example: Streaming Response — a typewriter / fade reveal of an assistant * reply, plus the "thinking" state before the first token. Per-story: the * Usage tab shows the snippet for the story you're on; the example-level fields * below are the fallback. */ const streamingResponse: ExampleUsage = { title: 'Examples/Streaming Response', ...typewriter, // example-level fallback = the headline "Typewriter Streaming" stories: { 'Typewriter Streaming': typewriter, 'Waiting for First Token': waiting, 'Fade-in Streaming': fade, 'Full Streaming Lifecycle': fullLifecycle, }, }; export default streamingResponse;