@kitnai/chat 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +35 -5
  2. package/dist/custom-elements.json +2969 -0
  3. package/dist/kitn-chat.es.js +52 -39
  4. package/dist/llms/llms-full.txt +718 -0
  5. package/dist/llms/llms.txt +104 -0
  6. package/dist/theme.tokens.css +137 -0
  7. package/frameworks/react/index.tsx +584 -0
  8. package/frameworks/react/runtime.tsx +94 -0
  9. package/llms-full.txt +718 -0
  10. package/llms.txt +104 -0
  11. package/package.json +53 -6
  12. package/src/components/attachments.tsx +4 -2
  13. package/src/components/chain-of-thought.tsx +1 -1
  14. package/src/components/chat-scope-picker.tsx +2 -2
  15. package/src/components/chat-thread.tsx +217 -0
  16. package/src/components/checkpoint.tsx +7 -3
  17. package/src/components/context.tsx +14 -18
  18. package/src/components/conversation-item.tsx +1 -1
  19. package/src/components/conversation-list.tsx +5 -4
  20. package/src/components/message-skills.tsx +1 -1
  21. package/src/components/message.tsx +1 -0
  22. package/src/components/model-switcher.tsx +3 -3
  23. package/src/components/prompt-input.tsx +20 -2
  24. package/src/components/reasoning.tsx +2 -2
  25. package/src/components/scroll-button.tsx +1 -0
  26. package/src/components/slash-command.tsx +17 -8
  27. package/src/components/source.tsx +2 -2
  28. package/src/components/thinking-bar.tsx +2 -2
  29. package/src/components/tool.tsx +17 -6
  30. package/src/components/voice-input.tsx +5 -1
  31. package/src/elements/attachments.tsx +132 -0
  32. package/src/elements/chain-of-thought.tsx +45 -0
  33. package/src/elements/chat-scope-picker.tsx +36 -0
  34. package/src/elements/chat-workspace.tsx +122 -0
  35. package/src/elements/chat.tsx +31 -228
  36. package/src/elements/checkpoint.tsx +43 -0
  37. package/src/elements/code-block.tsx +42 -0
  38. package/src/elements/compiled.css +1 -1
  39. package/src/elements/context-meter.tsx +71 -0
  40. package/src/elements/conversation-list.tsx +6 -0
  41. package/src/elements/default-input.tsx +22 -1
  42. package/src/elements/define.tsx +98 -12
  43. package/src/elements/element-types.d.ts +444 -0
  44. package/src/elements/empty.tsx +29 -0
  45. package/src/elements/feedback-bar.tsx +33 -0
  46. package/src/elements/file-upload.tsx +44 -0
  47. package/src/elements/image.tsx +32 -0
  48. package/src/elements/kitn-attachments.stories.tsx +181 -0
  49. package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
  50. package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
  51. package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
  52. package/src/elements/kitn-checkpoint.stories.tsx +71 -0
  53. package/src/elements/kitn-code-block.stories.tsx +82 -0
  54. package/src/elements/kitn-context-meter.stories.tsx +85 -0
  55. package/src/elements/kitn-empty.stories.tsx +110 -0
  56. package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
  57. package/src/elements/kitn-file-upload.stories.tsx +81 -0
  58. package/src/elements/kitn-image.stories.tsx +70 -0
  59. package/src/elements/kitn-loader.stories.tsx +87 -0
  60. package/src/elements/kitn-markdown.stories.tsx +75 -0
  61. package/src/elements/kitn-message-skills.stories.tsx +74 -0
  62. package/src/elements/kitn-message.stories.tsx +105 -0
  63. package/src/elements/kitn-model-switcher.stories.tsx +80 -0
  64. package/src/elements/kitn-prompt-input.stories.tsx +74 -16
  65. package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
  66. package/src/elements/kitn-reasoning.stories.tsx +76 -0
  67. package/src/elements/kitn-response-stream.stories.tsx +79 -0
  68. package/src/elements/kitn-source-list.stories.tsx +77 -0
  69. package/src/elements/kitn-source.stories.tsx +87 -0
  70. package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
  71. package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
  72. package/src/elements/kitn-tool.stories.tsx +88 -0
  73. package/src/elements/kitn-voice-input.stories.tsx +87 -0
  74. package/src/elements/loader.tsx +25 -0
  75. package/src/elements/markdown.tsx +38 -0
  76. package/src/elements/message-skills.tsx +22 -0
  77. package/src/elements/message.tsx +125 -0
  78. package/src/elements/model-switcher.tsx +35 -0
  79. package/src/elements/prompt-input.tsx +83 -7
  80. package/src/elements/prompt-suggestions.tsx +58 -0
  81. package/src/elements/reasoning.tsx +50 -0
  82. package/src/elements/register.ts +32 -0
  83. package/src/elements/response-stream.tsx +40 -0
  84. package/src/elements/source.tsx +67 -0
  85. package/src/elements/styles.css +14 -0
  86. package/src/elements/text-shimmer.tsx +28 -0
  87. package/src/elements/thinking-bar.tsx +34 -0
  88. package/src/elements/tool.tsx +23 -0
  89. package/src/elements/voice-input.tsx +41 -0
  90. package/src/index.ts +0 -1
  91. package/src/primitives/chat-config.tsx +3 -3
  92. package/src/stories/docs/Accessibility.mdx +119 -0
  93. package/src/stories/docs/ForAIAgents.mdx +93 -0
  94. package/src/stories/docs/GettingStarted.mdx +2 -2
  95. package/src/stories/docs/Installation.mdx +29 -2
  96. package/src/stories/docs/Integrations.mdx +417 -15
  97. package/src/stories/docs/Introduction.mdx +17 -8
  98. package/src/stories/docs/Theming.mdx +1 -1
  99. package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
  100. package/src/stories/pattern-docked-widget.stories.tsx +93 -0
  101. package/src/stories/pattern-empty-state.stories.tsx +76 -0
  102. package/src/stories/typography.stories.tsx +78 -0
  103. package/src/ui/button.tsx +1 -1
  104. package/src/ui/collapsible.stories.tsx +70 -0
  105. package/src/ui/collapsible.tsx +119 -8
  106. package/src/ui/dropdown.stories.tsx +60 -0
  107. package/src/ui/dropdown.tsx +177 -12
  108. package/src/ui/hover-card.stories.tsx +78 -0
  109. package/src/ui/hover-card.tsx +147 -26
  110. package/src/ui/overlay.stories.tsx +115 -0
  111. package/src/ui/overlay.tsx +151 -0
  112. package/src/ui/scroll-area.stories.tsx +51 -0
  113. package/src/ui/textarea.stories.tsx +77 -0
  114. package/src/ui/textarea.tsx +1 -1
  115. package/src/ui/tooltip.stories.tsx +1 -1
  116. package/src/ui/tooltip.tsx +59 -13
  117. package/src/utils/cn.ts +19 -1
  118. package/theme.css +76 -43
  119. package/src/ui/dialog.tsx +0 -21
@@ -1,37 +1,438 @@
1
1
  import { Meta } from '@storybook/addon-docs/blocks';
2
2
 
3
- <Meta title="Integrations" />
3
+ <Meta title="Docs/Frameworks & Integrations" />
4
4
 
5
- # Integrations
5
+ # Frameworks & Integrations
6
6
 
7
- The components don't talk to any model for you — they render `messages` and emit `submit`. That keeps you free to wire up any provider, streaming protocol, or extra like text-to-speech.
7
+ `@kitnai/chat` is transport-agnostic: every element renders `messages` and emits `submit`. You bring the model; the kit owns the UI. This page covers how to wire it up in plain HTML, React, Vue, Svelte, and Angular, then shows how to stream a real response.
8
+
9
+ ---
10
+
11
+ ## The #1 rule: properties vs. attributes
12
+
13
+ Arrays and objects **must** be set as JavaScript **properties** on the DOM element — never as HTML attributes. An HTML attribute is always a string, so passing `messages`, `models`, `context`, `suggestions`, or `slashCommands` as an attribute silently fails or is ignored.
14
+
15
+ ```js
16
+ const chat = document.querySelector('kitn-chat');
17
+
18
+ // ✅ correct — set as a JS property
19
+ chat.messages = [{ id: '1', role: 'assistant', content: 'Hello!' }];
20
+
21
+ // ❌ wrong — HTML attribute, always a string, never works
22
+ // <kitn-chat messages="[...]"></kitn-chat>
23
+ ```
24
+
25
+ Scalar values (strings, numbers, booleans) work as attributes: `placeholder`, `loading`, `theme`, `prose-size`, and so on.
26
+
27
+ ---
28
+
29
+ ## Plain HTML
30
+
31
+ Import the element bundle once as a side-effect (it registers all `kitn-*` custom elements), then set properties and listen for events in a `<script type="module">` block.
32
+
33
+ > **Want the whole shell in one tag?** `<kitn-chat-workspace>` bundles the conversation-list sidebar, the drag-to-resize handle, and the full chat thread together. Set `conversations`, `messages`, and optionally `models` as properties; listen for `conversationselect` and `submit`. See the <a href="?path=/docs/web-components-kitn-chat-workspace--docs">kitn-chat-workspace story</a> and the <a href="https://github.com/kitn-ai/kitn-chat/blob/main/docs/web-components.md#kitn-chat-workspace--kitnchatworkspace">web-components.md reference</a> for the full API.
34
+
35
+ ```html
36
+ <!DOCTYPE html>
37
+ <html>
38
+ <head>
39
+ <!-- optional: only needed to rebrand host-page markup -->
40
+ <link rel="stylesheet" href="./node_modules/@kitnai/chat/dist/theme.tokens.css" />
41
+ </head>
42
+ <body style="height: 100vh; margin: 0;">
43
+ <kitn-chat id="chat" style="display: block; height: 100%;"></kitn-chat>
44
+
45
+ <script type="module">
46
+ import '@kitnai/chat/elements';
47
+
48
+ const chat = document.getElementById('chat');
49
+
50
+ // Arrays and objects → JS properties
51
+ chat.messages = [
52
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?', actions: ['copy', 'like', 'dislike'] },
53
+ ];
54
+ chat.suggestions = ['Summarize the chat', 'Start fresh'];
55
+
56
+ // Events → addEventListener on the element (they don't bubble)
57
+ chat.addEventListener('submit', async (e) => {
58
+ const text = e.detail.value;
59
+
60
+ // Append user message (new array → triggers re-render)
61
+ const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
62
+ chat.messages = history;
63
+ chat.loading = true;
64
+
65
+ // Stream into an empty assistant placeholder
66
+ const aid = crypto.randomUUID();
67
+ chat.messages = [...history, { id: aid, role: 'assistant', content: '' }];
68
+
69
+ let answer = '';
70
+ for await (const token of streamFromYourAPI(history)) {
71
+ answer += token;
72
+ // Replace the placeholder with a new object each chunk
73
+ chat.messages = chat.messages.map((m) =>
74
+ m.id === aid ? { ...m, content: answer } : m
75
+ );
76
+ }
77
+ chat.loading = false;
78
+ });
79
+ </script>
80
+ </body>
81
+ </html>
82
+ ```
83
+
84
+ > **Reactivity:** always assign a **new array** (and a new object for any message you change). Mutating an existing object in place will not trigger a re-render.
85
+
86
+ ### Scalar attributes
87
+
88
+ Scalars can go directly in the HTML as attributes:
89
+
90
+ ```html
91
+ <!-- theme, placeholder, and loading are scalar → safe as attributes -->
92
+ <kitn-chat
93
+ theme="dark"
94
+ placeholder="Ask anything…"
95
+ ></kitn-chat>
96
+ ```
97
+
98
+ ---
99
+
100
+ ## React
101
+
102
+ The kit ships auto-generated, typed React wrappers under `@kitnai/chat/react`. They handle the ref plumbing internally — rich props are set as DOM **properties** and CustomEvents are exposed as `on<Event>` handlers, so you write idiomatic JSX without touching refs yourself.
103
+
104
+ ```tsx
105
+ import { KitnChat, KitnConversationList } from '@kitnai/chat/react';
106
+ import { useState } from 'react';
107
+
108
+ type Message = {
109
+ id: string;
110
+ role: 'user' | 'assistant';
111
+ content: string;
112
+ actions?: string[];
113
+ };
114
+
115
+ export function App() {
116
+ const [messages, setMessages] = useState<Message[]>([
117
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?', actions: ['copy'] },
118
+ ]);
119
+
120
+ const handleSubmit = async (e: CustomEvent<{ value: string }>) => {
121
+ const text = e.detail.value;
122
+
123
+ const history = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: text }];
124
+ setMessages(history);
125
+
126
+ // Placeholder to stream into
127
+ const aid = crypto.randomUUID();
128
+ setMessages([...history, { id: aid, role: 'assistant', content: '' }]);
129
+
130
+ let answer = '';
131
+ for await (const token of streamFromYourAPI(history)) {
132
+ answer += token;
133
+ setMessages((prev) =>
134
+ prev.map((m) => (m.id === aid ? { ...m, content: answer } : m))
135
+ );
136
+ }
137
+ };
138
+
139
+ return (
140
+ <KitnChat
141
+ messages={messages}
142
+ suggestions={['Summarize the chat', 'Start fresh']}
143
+ onSubmit={handleSubmit}
144
+ onMessageaction={(e) => console.log('action', e.detail)}
145
+ style={{ display: 'block', height: '100vh' }}
146
+ />
147
+ );
148
+ }
149
+ ```
150
+
151
+ ### Event prop naming
152
+
153
+ Event props follow the `on` + event-name pattern (camelCased):
154
+
155
+ | DOM event name | React prop |
156
+ |---|---|
157
+ | `submit` | `onSubmit` |
158
+ | `valuechange` | `onValuechange` |
159
+ | `messageaction` | `onMessageaction` |
160
+ | `modelchange` | `onModelchange` |
161
+ | `suggestionclick` | `onSuggestionclick` |
162
+ | `slashselect` | `onSlashselect` |
163
+ | `search` | `onSearch` |
164
+ | `voice` | `onVoice` |
165
+
166
+ ### All 27 elements have typed wrappers
167
+
168
+ Component names are the PascalCase of the tag name:
169
+
170
+ ```tsx
171
+ import {
172
+ KitnChat,
173
+ KitnConversationList,
174
+ KitnPromptInput,
175
+ KitnMessage,
176
+ KitnMarkdown,
177
+ KitnCodeBlock,
178
+ KitnReasoning,
179
+ KitnTool,
180
+ KitnContextMeter,
181
+ KitnModelSwitcher,
182
+ KitnAttachments,
183
+ KitnLoader,
184
+ // …all 27 elements
185
+ } from '@kitnai/chat/react';
186
+ ```
187
+
188
+ ### Without the React wrappers (raw custom element)
189
+
190
+ If you prefer to work with the raw custom element directly (e.g. with React 19's improved custom-element support), use a `ref` + `useEffect`:
191
+
192
+ ```tsx
193
+ import { useEffect, useRef, useState } from 'react';
194
+ import '@kitnai/chat/elements';
195
+
196
+ export function Chat() {
197
+ const chatRef = useRef<HTMLElement>(null);
198
+ const [messages, setMessages] = useState([
199
+ { id: '1', role: 'assistant', content: 'Hello!' },
200
+ ]);
201
+
202
+ // Set object/array properties — cannot go through JSX props
203
+ useEffect(() => {
204
+ const el = chatRef.current;
205
+ if (!el) return;
206
+ (el as any).messages = messages;
207
+ }, [messages]);
208
+
209
+ // Wire events with addEventListener
210
+ useEffect(() => {
211
+ const el = chatRef.current;
212
+ if (!el) return;
213
+
214
+ const onSubmit = (e: Event) => {
215
+ const { value } = (e as CustomEvent<{ value: string }>).detail;
216
+ setMessages((prev) => [
217
+ ...prev,
218
+ { id: crypto.randomUUID(), role: 'user', content: value },
219
+ ]);
220
+ };
221
+
222
+ el.addEventListener('submit', onSubmit);
223
+ return () => el.removeEventListener('submit', onSubmit);
224
+ }, []);
225
+
226
+ return <kitn-chat ref={chatRef} style={{ display: 'block', height: '100vh' }} />;
227
+ }
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Vue
233
+
234
+ In Vue, use the element directly in your template. Pass arrays and objects via the `.prop` modifier (or `:propName.prop` for dynamic bindings) so Vue sets them as DOM properties rather than HTML attributes. Events use the standard `@event` syntax.
235
+
236
+ ```html
237
+ <script setup lang="ts">
238
+ import { ref, onMounted } from 'vue';
239
+ import '@kitnai/chat/elements';
240
+
241
+ const messages = ref([
242
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?' },
243
+ ]);
244
+
245
+ const handleSubmit = (e: CustomEvent<{ value: string }>) => {
246
+ const text = e.detail.value;
247
+ messages.value = [
248
+ ...messages.value,
249
+ { id: crypto.randomUUID(), role: 'user', content: text },
250
+ ];
251
+ // …stream a reply and append an assistant message
252
+ };
253
+ </script>
254
+
255
+ <template>
256
+ <!-- Arrays/objects → .prop modifier; scalars → plain attributes -->
257
+ <kitn-chat
258
+ :messages.prop="messages"
259
+ placeholder="Ask anything…"
260
+ theme="auto"
261
+ style="display: block; height: 100vh"
262
+ @submit="handleSubmit"
263
+ />
264
+ </template>
265
+ ```
266
+
267
+ ### Vue + TypeScript: augmenting JSX types
268
+
269
+ Add `@kitnai/chat/elements` to your `vite.config.ts` / `env.d.ts` once so Vue's template compiler knows the element's attributes:
270
+
271
+ ```ts
272
+ // env.d.ts (or vite-env.d.ts)
273
+ /// <reference types="@kitnai/chat/elements" />
274
+ ```
275
+
276
+ ### Sidebar + chat together (Vue)
277
+
278
+ Cross-element coordination goes through the host component — the `<kitn-conversation-list>` fires `select`, you update a reactive ref, and pass it into `<kitn-chat>`:
279
+
280
+ ```html
281
+ <script setup lang="ts">
282
+ import '@kitnai/chat/elements';
283
+ import { ref } from 'vue';
284
+
285
+ const conversations = ref([
286
+ { id: 'c1', title: 'First chat', scope: { type: 'document' }, messageCount: 3,
287
+ lastMessageAt: '2026-06-01T12:00:00Z', updatedAt: '2026-06-01T12:00:00Z' },
288
+ ]);
289
+ const activeId = ref('c1');
290
+ const messages = ref([{ id: '1', role: 'assistant', content: 'Hi!' }]);
291
+
292
+ const onSelect = (e: CustomEvent<{ id: string }>) => {
293
+ activeId.value = e.detail.id;
294
+ // load messages for the selected conversation
295
+ };
296
+ </script>
297
+
298
+ <template>
299
+ <div style="display: flex; height: 100vh;">
300
+ <kitn-conversation-list
301
+ :conversations.prop="conversations"
302
+ :active-id="activeId"
303
+ style="width: 260px"
304
+ @select="onSelect"
305
+ />
306
+ <kitn-chat
307
+ :messages.prop="messages"
308
+ style="flex: 1"
309
+ />
310
+ </div>
311
+ </template>
312
+ ```
313
+
314
+ ---
315
+
316
+ ## Svelte
317
+
318
+ Svelte's template compiler sets DOM **properties** when you bind with `bind:` or use the `|propname` directive. For custom events, use `on:eventname`:
319
+
320
+ ```html
321
+ <script>
322
+ import '@kitnai/chat/elements';
323
+
324
+ let messages = [
325
+ { id: '1', role: 'assistant', content: 'Hello!' }
326
+ ];
327
+
328
+ function handleSubmit(e) {
329
+ const text = e.detail.value;
330
+ messages = [...messages, { id: crypto.randomUUID(), role: 'user', content: text }];
331
+ // …stream reply
332
+ }
333
+ </script>
334
+
335
+ <!-- use:action pattern to set properties -->
336
+ <kitn-chat
337
+ use:setProps={{ messages }}
338
+ style="display: block; height: 100vh"
339
+ on:submit={handleSubmit}
340
+ />
341
+
342
+ <script context="module">
343
+ function setProps(node, props) {
344
+ Object.assign(node, props);
345
+ return {
346
+ update(newProps) { Object.assign(node, newProps); }
347
+ };
348
+ }
349
+ </script>
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Angular
355
+
356
+ Angular needs **no wrappers** — its property binding (`[prop]="value"`) assigns the DOM **property** directly, so arrays and objects pass through unstringified. Register the elements once, allow the custom tags with `CUSTOM_ELEMENTS_SCHEMA`, and handle events with `(event)="handler($event)"` (the payload is on `$event.detail`).
357
+
358
+ ```ts
359
+ // main.ts — register the kitn-* elements once, app-wide
360
+ import '@kitnai/chat/elements';
361
+ ```
362
+
363
+ ```ts
364
+ // app.component.ts
365
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
366
+
367
+ @Component({
368
+ selector: 'app-root',
369
+ standalone: true,
370
+ templateUrl: './app.component.html',
371
+ schemas: [CUSTOM_ELEMENTS_SCHEMA], // allow the <kitn-*> custom elements
372
+ })
373
+ export class AppComponent {
374
+ messages = [{ id: '1', role: 'assistant', content: 'Hello!' }];
375
+ models = [{ id: 'opus', name: 'Claude Opus' }];
376
+ loading = false;
377
+
378
+ onSubmit(e: Event) {
379
+ const { value } = (e as CustomEvent<{ value: string }>).detail;
380
+ // Reassign a NEW array so change detection re-renders.
381
+ this.messages = [
382
+ ...this.messages,
383
+ { id: crypto.randomUUID(), role: 'user', content: value },
384
+ ];
385
+ // …stream the reply, reassigning this.messages with a new array each chunk
386
+ }
387
+
388
+ onModelChange(e: Event) {
389
+ console.log('model:', (e as CustomEvent<{ modelId: string }>).detail.modelId);
390
+ }
391
+ }
392
+ ```
393
+
394
+ ```html
395
+ <!-- app.component.html -->
396
+ <kitn-chat
397
+ [messages]="messages"
398
+ [models]="models"
399
+ [loading]="loading"
400
+ theme="auto"
401
+ style="display: block; height: 100vh"
402
+ (submit)="onSubmit($event)"
403
+ (modelchange)="onModelChange($event)"
404
+ ></kitn-chat>
405
+ ```
406
+
407
+ > **Why `[messages]` works:** Angular's `[prop]` binding writes to the element's DOM property (not an attribute), so the property-vs-attribute rule is satisfied automatically. Scalars like `theme` can be plain attributes. `CUSTOM_ELEMENTS_SCHEMA` is required so Angular doesn't error on the unknown `kitn-*` tags.
408
+
409
+ ---
8
410
 
9
411
  ## Streaming from OpenRouter
10
412
 
11
- [OpenRouter](https://openrouter.ai) exposes an OpenAI-compatible streaming API (Server-Sent Events). On `submit`, append the user message plus an empty assistant message, then grow the assistant message as tokens arrive.
413
+ [OpenRouter](https://openrouter.ai) exposes an OpenAI-compatible streaming API (Server-Sent Events). Wire it into the `submit` event:
12
414
 
13
- > **Security:** never ship an API key to the browser. In production, point `fetch` at your own backend that proxies to OpenRouter and injects the key. The parsing is identical either way.
415
+ > **Security:** never ship an API key to the browser. In production, point `fetch` at your own backend that proxies to OpenRouter and injects the key.
14
416
 
15
417
  ```js
16
418
  chat.addEventListener('submit', async (e) => {
17
419
  const text = e.detail.value.trim();
18
420
  if (!text) return;
19
421
 
20
- // 1. Show the user message; clear the input
422
+ // 1. Show the user message
21
423
  const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
22
424
  chat.messages = history;
23
- chat.value = '';
24
425
  chat.loading = true;
25
426
 
26
- // 2. Add an empty assistant message to stream into
427
+ // 2. Empty assistant placeholder to stream into
27
428
  const assistantId = crypto.randomUUID();
28
429
  chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];
29
430
 
30
- // In production, replace this URL with your own proxy endpoint.
431
+ // In production, replace this with your own proxy endpoint.
31
432
  const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
32
433
  method: 'POST',
33
434
  headers: {
34
- 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, // server-side in production!
435
+ 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
35
436
  'Content-Type': 'application/json',
36
437
  },
37
438
  body: JSON.stringify({
@@ -52,17 +453,16 @@ chat.addEventListener('submit', async (e) => {
52
453
  buffer += decoder.decode(value, { stream: true });
53
454
 
54
455
  const lines = buffer.split('\n');
55
- buffer = lines.pop(); // keep the partial last line
456
+ buffer = lines.pop();
56
457
  for (const line of lines) {
57
458
  const s = line.trim();
58
- if (!s.startsWith('data:')) continue; // skip keep-alive comments
459
+ if (!s.startsWith('data:')) continue;
59
460
  const payload = s.slice(5).trim();
60
461
  if (payload === '[DONE]') continue;
61
462
  try {
62
463
  const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
63
464
  if (!delta) continue;
64
465
  answer += delta;
65
- // New object for the streaming message so the row re-renders
66
466
  chat.messages = chat.messages.map((m) =>
67
467
  m.id === assistantId ? { ...m, content: answer } : m
68
468
  );
@@ -73,6 +473,8 @@ chat.addEventListener('submit', async (e) => {
73
473
  });
74
474
  ```
75
475
 
476
+ ---
477
+
76
478
  ## Text-to-speech (TTS)
77
479
 
78
480
  ### Browser-native (zero dependencies)
@@ -91,7 +493,7 @@ function speak(text) {
91
493
 
92
494
  ### Cloud TTS (OpenAI, ElevenLabs, …)
93
495
 
94
- For higher-quality voices, have your backend call a TTS API and return audio, then play it (keep the provider key server-side):
496
+ For higher-quality voices, have your backend call a TTS API and return audio (keep the provider key server-side):
95
497
 
96
498
  ```js
97
499
  async function speakCloud(text) {
@@ -107,4 +509,4 @@ async function speakCloud(text) {
107
509
 
108
510
  ## Speech-to-text
109
511
 
110
- The reverse direction is built in — the kit ships a `VoiceInput` component for capturing microphone input. Find it in the sidebar under the component stories.
512
+ The reverse direction is built in — the kit ships `<kitn-voice-input>` (and a `VoiceInput` SolidJS component). Find it in the sidebar under the component stories.
@@ -1,11 +1,11 @@
1
1
  import { Meta, Canvas } from '@storybook/addon-docs/blocks';
2
2
  import * as ChatPanel from '../chat-panel-layout.stories';
3
3
 
4
- <Meta title="Introduction" />
4
+ <Meta title="Docs/Introduction" />
5
5
 
6
6
  # @kitnai/chat
7
7
 
8
- **A SolidJS + Shadow-DOM web component kit for building AI chat interfaces.**
8
+ **Framework-agnostic, Shadow-DOM web components for building AI chat interfaces.**
9
9
 
10
10
  Message threads, prompt inputs, streaming responses, markdown + code rendering, reasoning & tool-call panels, attachments, and a conversation sidebar — composable building blocks you can drop into any app.
11
11
 
@@ -13,17 +13,26 @@ Message threads, prompt inputs, streaming responses, markdown + code rendering,
13
13
 
14
14
  ## Why @kitnai/chat
15
15
 
16
- - **Two ways to use it** — import the SolidJS components for full compositional control, or drop in framework-agnostic **web components** (`<kitn-chat>`) that work in React, Vue, Svelte, or plain HTML.
16
+ - **Works in any framework** — drop in the framework-agnostic **web components** (`<kitn-chat>`) and they just work in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS, so SolidJS apps can also import the components natively for full compositional control.
17
17
  - **Zero style conflicts** — the web components render in **Shadow DOM**, so the host page's CSS can't leak in and the kit's Tailwind can't leak out.
18
18
  - **Lightweight** — a markdown-only `<kitn-chat>` is **~61 KB gzip**, a single file. Syntax highlighting loads **on demand, per language, with no WASM** — and never loads at all if you don't render code.
19
- - **~50 composable components** across three layers: headless primitives → UI primitives (built on [Kobalte](https://kobalte.dev)) → AI feature components.
19
+ - **~50 composable components** across three layers: headless primitives → accessible UI primitives (built in-house, WCAG 2.1 AA — no third-party UI dependency) → AI feature components.
20
20
  - **Themeable** — restyle everything by overriding a handful of `--color-*` design tokens.
21
21
 
22
+ ## Browsing the sidebar — which group do I copy from?
23
+
24
+ The kit ships at two layers, and the sidebar reflects that. **Use the right one for your stack:**
25
+
26
+ - **Web Components** — the framework-agnostic `<kitn-*>` custom elements. **This is what to copy into a React, Vue, Angular, Svelte, or plain-HTML app.** Data goes on JS properties, interactions come back as events.
27
+ - **Components · SolidJS** and **UI · SolidJS** — the **native SolidJS** components (feature components) and primitives (Button, Dropdown, HoverCard, …) that the web components are *built from*. Their snippets are SolidJS JSX, so **only copy these into a SolidJS app**.
28
+
29
+ In other words: the `<kitn-chat>` web component is a thin facade over the SolidJS `ChatContainer`/`Message`/… components — same UI, different consumption model. When in doubt, reach for **Web Components**.
30
+
22
31
  ## Where to next
23
32
 
24
- - **[Installation](?path=/docs/installation--docs)** — add it to your project
25
- - **[Getting Started](?path=/docs/getting-started--docs)** — your first chat in a few lines, plus a full example
26
- - **[Theming](?path=/docs/theming--docs)** — make it match your brand
27
- - **[Integrations](?path=/docs/integrations--docs)** — stream responses from OpenRouter and add text-to-speech
33
+ - **[Installation](?path=/docs/docs-installation--docs)** — add it to your project
34
+ - **[Getting Started](?path=/docs/docs-getting-started--docs)** — your first chat in a few lines, plus a full example
35
+ - **[Theming](?path=/docs/docs-theming--docs)** — make it match your brand
36
+ - **[Integrations](?path=/docs/docs-frameworks-integrations--docs)** — stream responses from OpenRouter and add text-to-speech
28
37
 
29
38
  Or browse every component in isolation from the sidebar.
@@ -1,7 +1,7 @@
1
1
  import { Meta, Canvas } from '@storybook/addon-docs/blocks';
2
2
  import * as TokenRef from '../token-reference.stories';
3
3
 
4
- <Meta title="Theming" />
4
+ <Meta title="Docs/Theming" />
5
5
 
6
6
  # Theming
7
7
 
@@ -0,0 +1,93 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { createSignal } from 'solid-js';
3
+ import {
4
+ ChatConfig, ChatContainer, ChatContainerContent, ChatContainerScrollAnchor,
5
+ Message, MessageContent, MessageActions,
6
+ PromptInput, PromptInputTextarea, PromptInputActions,
7
+ ModelSwitcher, Button,
8
+ } from '../index';
9
+ import type { ModelOption } from '../types';
10
+ import { Copy, ThumbsUp, ArrowUp } from 'lucide-solid';
11
+
12
+ const meta: Meta = {
13
+ title: 'Patterns/Centered Conversation',
14
+ parameters: {
15
+ layout: 'centered',
16
+ docs: {
17
+ description: {
18
+ component:
19
+ 'A single, centered reading column with no sidebar (Claude.ai-style). The messages and composer share one `max-w` measure centered in the viewport — ideal for a focused, full-window chat. Contrast with **Chat Panel Layout** (a compact embedded panel) and the **Full Chat App** example (a resizable workspace).',
20
+ },
21
+ },
22
+ },
23
+ };
24
+ export default meta;
25
+ type Story = StoryObj;
26
+
27
+ const models: ModelOption[] = [
28
+ { id: 'claude-4', name: 'Claude 4 Opus', provider: 'Anthropic' },
29
+ { id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' },
30
+ ];
31
+
32
+ const answer = `Sure — here's the gist:
33
+
34
+ - **Signals** are fine-grained, so only the DOM that reads a signal updates.
35
+ - **No re-renders** — components run once; reactive expressions do the work.
36
+ - **No dependency arrays** — effects auto-track what they read.
37
+
38
+ Want a code example next?`;
39
+
40
+ export const Focused: Story = {
41
+ render: () => {
42
+ const [input, setInput] = createSignal('');
43
+ const [model, setModel] = createSignal('claude-4');
44
+ return (
45
+ <ChatConfig proseSize="base">
46
+ <div style={{ width: '860px', height: '680px' }} class="flex flex-col overflow-hidden rounded-xl border border-border bg-background">
47
+ <header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
48
+ <span class="text-sm font-semibold text-foreground">New conversation</span>
49
+ <ModelSwitcher models={models} currentModelId={model()} onModelChange={setModel} />
50
+ </header>
51
+
52
+ <div class="relative flex-1 overflow-y-auto">
53
+ <ChatContainer class="h-full">
54
+ <ChatContainerContent class="space-y-6 px-5 py-6">
55
+ <Message class="mx-auto flex w-full max-w-2xl flex-col items-end">
56
+ <MessageContent class="bg-muted text-primary max-w-[85%] rounded-3xl px-5 py-2.5">
57
+ In one paragraph, why is SolidJS reactivity fast?
58
+ </MessageContent>
59
+ </Message>
60
+
61
+ <Message class="mx-auto flex w-full max-w-2xl flex-col items-start">
62
+ <div class="group flex w-full flex-col">
63
+ <MessageContent markdown class="bg-transparent p-0 text-foreground">
64
+ {answer}
65
+ </MessageContent>
66
+ <MessageActions class="-ml-2.5 flex gap-0 opacity-0 transition-opacity duration-150 group-hover:opacity-100">
67
+ <Button variant="ghost" size="icon-sm" class="rounded-full"><Copy class="size-3.5" /></Button>
68
+ <Button variant="ghost" size="icon-sm" class="rounded-full"><ThumbsUp class="size-3.5" /></Button>
69
+ </MessageActions>
70
+ </div>
71
+ </Message>
72
+ <ChatContainerScrollAnchor />
73
+ </ChatContainerContent>
74
+ </ChatContainer>
75
+ </div>
76
+
77
+ <div class="shrink-0 px-5 pb-5">
78
+ <div class="mx-auto max-w-2xl">
79
+ <PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
80
+ <PromptInputTextarea placeholder="Reply…" class="min-h-[44px] pt-3 pl-4" />
81
+ <PromptInputActions class="mt-2 flex w-full items-center justify-end gap-2 px-3 pb-3">
82
+ <Button size="icon-sm" class="rounded-full" disabled={!input().trim()}>
83
+ <ArrowUp class="size-4" />
84
+ </Button>
85
+ </PromptInputActions>
86
+ </PromptInput>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </ChatConfig>
91
+ );
92
+ },
93
+ };