UNPKG

@kitn.ai/chat

Version:

Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.

278 lines (248 loc) 10.7 kB
import type { ExampleUsage, StoryUsage } from './types'; // A small thread reused across the snippets — user + assistant, with actions, // reasoning, and a tool call on the second assistant message. const MESSAGES = `[ { id: 'u1', role: 'user', content: 'Can you explain how SolidJS reactivity differs from React hooks?' }, { id: 'a1', role: 'assistant', content: 'SolidJS uses fine-grained signals: components run once and only the DOM nodes that read a signal update.', actions: ['copy', 'like', 'dislike', 'regenerate'], }, { id: 'u2', role: 'user', content: 'Can you benchmark SolidJS vs React for 10,000 items?' }, { id: 'a2', role: 'assistant', content: 'Here are the benchmark results — SolidJS renders ~7x faster and updates ~42x faster.', actions: ['copy', 'like', 'dislike', 'regenerate'], reasoning: { label: 'Thought for 4 seconds', text: 'Render the same 10,000 items in both frameworks for a fair comparison.' }, tools: [ { type: 'run_benchmark', state: 'output-available', input: { framework: ['solid', 'react'], itemCount: 10000 }, output: { solid: { avgRenderMs: 12.4 }, react: { avgRenderMs: 89.2 } }, toolCallId: 'call_abc123', }, ], }, ]`; const MODELS = `[ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' }, { id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' }, ]`; const CONTEXT = `{ usedTokens: 12400, maxTokens: 200000, inputTokens: 8200, outputTokens: 4200, estimatedCost: 0.042 }`; const SUGGESTIONS = `['How does SolidJS handle context?', 'Show me a store example']`; /** * Default — the entire chat experience in one element. `<kc-chat>` renders the * message thread (markdown, reasoning, tool calls, per-message action bars), a * header with the `models` switcher + `context` token meter, the `suggestions` * chips, and the prompt input. The demo's resizable sidebar + conversation list * are separate chrome that lives *outside* the element. */ const fullChat: StoryUsage = { intro: 'Drop in the whole chat experience with one element. `<kc-chat>` renders the thread (markdown, reasoning, tool calls, action bars), an optional header model switcher + `context` token meter, `suggestions` chips, and the prompt input — set `messages` as a property and handle `submit` / `messageaction` / `modelchange`. The resizable conversation sidebar in the demo is separate chrome `<kc-chat>` does not include. (The live demo composes the SolidJS `ChatContainer`, `Message`, `PromptInput`, and `ConversationList` primitives directly.)', snippets: { html: `<!-- Register the elements once (CDN or bundler) --> <script type="module"> import 'https://cdn.jsdelivr.net/npm/@kitn.ai/chat/dist/kitn-chat.es.js'; </script> <kc-chat id="chat" chat-title="SolidJS reactivity vs React hooks" current-model="claude-4" prose-size="base" placeholder="Ask anything..." ></kc-chat> <script type="module"> const chat = document.getElementById('chat'); // Object/array props are set as PROPERTIES, not attributes. chat.messages = ${MESSAGES}; chat.models = ${MODELS}; chat.context = ${CONTEXT}; chat.suggestions = ${SUGGESTIONS}; chat.addEventListener('kc-submit', (e) => { const { value, attachments } = e.detail; // append the user message / call your backend console.log('send', value, attachments); }); chat.addEventListener('kc-message-action', (e) => { const { messageId, action } = e.detail; // 'copy' | 'like' | 'dislike' | 'regenerate' | 'edit' console.log(messageId, action); }); chat.addEventListener('kc-model-change', (e) => console.log(e.detail.modelId)); </script>`, react: `import { Chat } from '@kitn.ai/chat/react'; export function ChatApp() { return ( <Chat chatTitle="SolidJS reactivity vs React hooks" proseSize="base" placeholder="Ask anything..." currentModel="claude-4" models={${MODELS}} context={${CONTEXT}} suggestions={${SUGGESTIONS}} messages={${MESSAGES}} onSubmit={(e) => console.log('send', e.detail.value, e.detail.attachments)} onMessageAction={(e) => console.log(e.detail.messageId, e.detail.action)} onModelChange={(e) => console.log(e.detail.modelId)} /> ); }`, vue: `<script setup> import '@kitn.ai/chat/elements'; // register once (e.g. in main.ts) // Object/array props use the .prop modifier in the template. const messages = ${MESSAGES}; const models = ${MODELS}; const context = ${CONTEXT}; const suggestions = ${SUGGESTIONS}; function onSubmit(e) { console.log('send', e.detail.value, e.detail.attachments); } function onAction(e) { console.log(e.detail.messageId, e.detail.action); } </script> <template> <kc-chat chat-title="SolidJS reactivity vs React hooks" prose-size="base" placeholder="Ask anything..." current-model="claude-4" :messages.prop="messages" :models.prop="models" :context.prop="context" :suggestions.prop="suggestions" @kc-submit="onSubmit" @kc-message-action="onAction" @kc-model-change="(e) => console.log(e.detail.modelId)" /> </template>`, svelte: `<script> import '@kitn.ai/chat/elements'; let el; // Object/array props are set as properties via bind:this. const messages = ${MESSAGES}; const models = ${MODELS}; const context = ${CONTEXT}; const suggestions = ${SUGGESTIONS}; $: if (el) { el.messages = messages; el.models = models; el.context = context; el.suggestions = suggestions; } function onSubmit(e) { console.log('send', e.detail.value, e.detail.attachments); } function onAction(e) { console.log(e.detail.messageId, e.detail.action); } </script> <kc-chat bind:this={el} chat-title="SolidJS reactivity vs React hooks" prose-size="base" placeholder="Ask anything..." current-model="claude-4" on:kc-submit={onSubmit} on:kc-message-action={onAction} on:kc-model-change={(e) => console.log(e.detail.modelId)} />`, 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-chat', standalone: true, schemas: [CUSTOM_ELEMENTS_SCHEMA], template: \` <kc-chat chat-title="SolidJS reactivity vs React hooks" prose-size="base" placeholder="Ask anything..." current-model="claude-4" [messages]="messages" [models]="models" [context]="context" [suggestions]="suggestions" (kc-submit)="onSubmit($event)" (kc-message-action)="onAction($event)" (kc-model-change)="onModel($event)" ></kc-chat> \`, }) export class ChatComponent { messages = ${MESSAGES}; models = ${MODELS}; context = ${CONTEXT}; suggestions = ${SUGGESTIONS}; onSubmit(e: CustomEvent<{ value: string; attachments: unknown[] }>) { console.log('send', e.detail.value, e.detail.attachments); } onAction(e: CustomEvent<{ messageId: string; action: string }>) { console.log(e.detail.messageId, e.detail.action); } onModel(e: CustomEvent<{ modelId: string }>) { console.log(e.detail.modelId); } }`, solid: `import { createSignal, For } from 'solid-js'; import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor, Message, MessageContent, MessageActions, PromptInput, PromptInputTextarea, PromptInputActions, ConversationList, ModelSwitcher, PromptSuggestion, ScrollButton, Button, } from '@kitn.ai/chat'; import { Copy, ThumbsUp, ThumbsDown, RefreshCw, ArrowUp } from 'lucide-solid'; export function ChatApp() { const [value, setValue] = createSignal(''); const [modelId, setModelId] = createSignal('claude-4'); return ( <div class="flex h-screen w-full bg-background"> {/* Sidebar — separate chrome from the thread */} <ConversationList groups={groups} conversations={conversations} activeId="1" onSelect={() => {}} onNewChat={() => {}} /> <main class="flex flex-1 flex-col overflow-hidden"> <header class="flex h-14 items-center justify-between border-b border-border px-5"> <div class="text-sm font-semibold">SolidJS reactivity vs React hooks</div> <ModelSwitcher models={models} currentModelId={modelId()} onModelChange={setModelId} /> </header> <ChatContainer class="flex-1"> <ChatContainerContent class="px-5 pt-4"> <Message class="group items-start"> <MessageContent markdown class="prose">{assistantReply}</MessageContent> <MessageActions class="opacity-0 group-hover:opacity-100 transition-opacity"> <Button variant="ghost" size="icon-sm" aria-label="Copy"><Copy class="size-3.5" /></Button> <Button variant="ghost" size="icon-sm" aria-label="Good response"><ThumbsUp class="size-3.5" /></Button> <Button variant="ghost" size="icon-sm" aria-label="Bad response"><ThumbsDown class="size-3.5" /></Button> <Button variant="ghost" size="icon-sm" aria-label="Regenerate"><RefreshCw class="size-3.5" /></Button> </MessageActions> </Message> <ChatContainerScrollAnchor /> </ChatContainerContent> <ScrollButton /> </ChatContainer> <div class="px-5 pb-5"> <div class="flex gap-2 pb-3"> <For each={['How does SolidJS handle context?', 'Show me a store example']}> {(s) => <PromptSuggestion onClick={() => setValue(s)}>{s}</PromptSuggestion>} </For> </div> <PromptInput value={value()} onValueChange={setValue} onSubmit={() => setValue('')}> <PromptInputTextarea placeholder="Ask anything..." /> <PromptInputActions class="justify-end"> <Button size="icon-sm" disabled={!value().trim()} aria-label="Send message"><ArrowUp class="size-4" /></Button> </PromptInputActions> </PromptInput> </div> </main> </div> ); }`, }, }; /** * Example: Full Chat App — the complete experience. As an embedder you reach for * the single `<kc-chat>` element; the live demo composes the granular SolidJS * primitives for full control. Per-story: the Usage tab shows the snippet for the * story you're on; the example-level fields below are the fallback. */ const fullChatApp: ExampleUsage = { title: 'Examples/Full Chat App', ...fullChat, // example-level fallback = the "Default" story stories: { Default: fullChat, }, }; export default fullChatApp;