@kitnai/chat 0.3.0 → 0.4.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 (106) hide show
  1. package/README.md +11 -0
  2. package/dist/custom-elements.json +2494 -0
  3. package/dist/kitn-chat.es.js +52 -39
  4. package/dist/llms/llms-full.txt +667 -0
  5. package/dist/llms/llms.txt +104 -0
  6. package/dist/theme.tokens.css +133 -0
  7. package/frameworks/react/index.tsx +530 -0
  8. package/frameworks/react/runtime.tsx +94 -0
  9. package/llms-full.txt +667 -0
  10. package/llms.txt +104 -0
  11. package/package.json +34 -5
  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/checkpoint.tsx +7 -3
  16. package/src/components/context.tsx +14 -18
  17. package/src/components/conversation-item.tsx +1 -1
  18. package/src/components/conversation-list.tsx +5 -4
  19. package/src/components/message-skills.tsx +1 -1
  20. package/src/components/message.tsx +1 -0
  21. package/src/components/model-switcher.tsx +3 -3
  22. package/src/components/prompt-input.tsx +15 -2
  23. package/src/components/reasoning.tsx +2 -2
  24. package/src/components/scroll-button.tsx +1 -0
  25. package/src/components/slash-command.tsx +17 -8
  26. package/src/components/source.tsx +2 -2
  27. package/src/components/thinking-bar.tsx +2 -2
  28. package/src/components/tool.tsx +17 -6
  29. package/src/components/voice-input.tsx +5 -1
  30. package/src/elements/attachments.tsx +132 -0
  31. package/src/elements/chain-of-thought.tsx +45 -0
  32. package/src/elements/chat-scope-picker.tsx +36 -0
  33. package/src/elements/chat.tsx +51 -7
  34. package/src/elements/checkpoint.tsx +43 -0
  35. package/src/elements/code-block.tsx +42 -0
  36. package/src/elements/compiled.css +1 -1
  37. package/src/elements/context-meter.tsx +71 -0
  38. package/src/elements/conversation-list.tsx +6 -0
  39. package/src/elements/default-input.tsx +22 -1
  40. package/src/elements/define.tsx +102 -13
  41. package/src/elements/element-types.d.ts +404 -0
  42. package/src/elements/empty.tsx +29 -0
  43. package/src/elements/feedback-bar.tsx +33 -0
  44. package/src/elements/file-upload.tsx +44 -0
  45. package/src/elements/image.tsx +32 -0
  46. package/src/elements/kitn-attachments.stories.tsx +181 -0
  47. package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
  48. package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
  49. package/src/elements/kitn-checkpoint.stories.tsx +71 -0
  50. package/src/elements/kitn-code-block.stories.tsx +82 -0
  51. package/src/elements/kitn-context-meter.stories.tsx +85 -0
  52. package/src/elements/kitn-empty.stories.tsx +110 -0
  53. package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
  54. package/src/elements/kitn-file-upload.stories.tsx +81 -0
  55. package/src/elements/kitn-image.stories.tsx +70 -0
  56. package/src/elements/kitn-loader.stories.tsx +87 -0
  57. package/src/elements/kitn-markdown.stories.tsx +75 -0
  58. package/src/elements/kitn-message-skills.stories.tsx +74 -0
  59. package/src/elements/kitn-message.stories.tsx +105 -0
  60. package/src/elements/kitn-model-switcher.stories.tsx +80 -0
  61. package/src/elements/kitn-prompt-input.stories.tsx +74 -16
  62. package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
  63. package/src/elements/kitn-reasoning.stories.tsx +76 -0
  64. package/src/elements/kitn-response-stream.stories.tsx +79 -0
  65. package/src/elements/kitn-source-list.stories.tsx +77 -0
  66. package/src/elements/kitn-source.stories.tsx +87 -0
  67. package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
  68. package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
  69. package/src/elements/kitn-tool.stories.tsx +88 -0
  70. package/src/elements/kitn-voice-input.stories.tsx +87 -0
  71. package/src/elements/loader.tsx +25 -0
  72. package/src/elements/markdown.tsx +38 -0
  73. package/src/elements/message-skills.tsx +22 -0
  74. package/src/elements/message.tsx +125 -0
  75. package/src/elements/model-switcher.tsx +35 -0
  76. package/src/elements/prompt-input.tsx +83 -7
  77. package/src/elements/prompt-suggestions.tsx +58 -0
  78. package/src/elements/reasoning.tsx +50 -0
  79. package/src/elements/register.ts +31 -0
  80. package/src/elements/response-stream.tsx +40 -0
  81. package/src/elements/source.tsx +67 -0
  82. package/src/elements/text-shimmer.tsx +28 -0
  83. package/src/elements/thinking-bar.tsx +34 -0
  84. package/src/elements/tool.tsx +23 -0
  85. package/src/elements/voice-input.tsx +41 -0
  86. package/src/index.ts +0 -1
  87. package/src/primitives/chat-config.tsx +2 -2
  88. package/src/stories/docs/Accessibility.mdx +119 -0
  89. package/src/stories/docs/ForAIAgents.mdx +93 -0
  90. package/src/stories/docs/GettingStarted.mdx +2 -2
  91. package/src/stories/docs/Installation.mdx +2 -2
  92. package/src/stories/docs/Integrations.mdx +415 -15
  93. package/src/stories/docs/Introduction.mdx +5 -5
  94. package/src/stories/docs/Theming.mdx +1 -1
  95. package/src/stories/typography.stories.tsx +78 -0
  96. package/src/ui/button.tsx +1 -1
  97. package/src/ui/collapsible.tsx +119 -8
  98. package/src/ui/dropdown.tsx +177 -12
  99. package/src/ui/hover-card.tsx +147 -26
  100. package/src/ui/overlay.tsx +151 -0
  101. package/src/ui/textarea.tsx +1 -1
  102. package/src/ui/tooltip.stories.tsx +1 -1
  103. package/src/ui/tooltip.tsx +59 -13
  104. package/src/utils/cn.ts +19 -1
  105. package/theme.css +72 -43
  106. package/src/ui/dialog.tsx +0 -21
@@ -0,0 +1,93 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Docs/For AI Agents" />
4
+
5
+ # For AI Agents / LLMs
6
+
7
+ `@kitnai/chat` ships machine-readable orientation files that follow the
8
+ [llmstxt.org](https://llmstxt.org) convention, so coding agents (Claude Code,
9
+ Copilot, Cursor, Codex, …) can wire up the components correctly without guessing.
10
+
11
+ **View them now:** <a href="/llms.txt" target="_blank" rel="noreferrer">llms.txt</a> ·
12
+ <a href="/llms-full.txt" target="_blank" rel="noreferrer">llms-full.txt</a>
13
+ (open in a new tab)
14
+
15
+ ## The files
16
+
17
+ | File | Audience | Where to find it |
18
+ |---|---|---|
19
+ | <a href="/llms.txt" target="_blank" rel="noreferrer"><code>llms.txt</code></a> | Agents + humans | Repo root, the npm package root (`node_modules/@kitnai/chat/llms.txt`), and `https://kitn.dev/llms.txt` |
20
+ | <a href="/llms-full.txt" target="_blank" rel="noreferrer"><code>llms-full.txt</code></a> | Agents | Same locations; the per-element API reference for **every** `kitn-*` element |
21
+
22
+ Both files are **auto-generated** from `dist/custom-elements.json` by
23
+ `scripts/gen-llms.mjs` during `npm run build`, so they never drift from the
24
+ shipped API. Do not edit them by hand.
25
+
26
+ - **`llms.txt`** (~4 KB) — a dense orientation: install, the property-vs-attribute
27
+ rule, the two-layer architecture, theming, and framework wiring. Fast to read.
28
+ - **`llms-full.txt`** (~32 KB) — everything in `llms.txt` plus a generated props/events
29
+ table for each element, a streaming recipe, and a "build a chat app" runbook.
30
+
31
+ ## The #1 rule agents get wrong
32
+
33
+ **Array and object data must be set as JavaScript properties, not HTML attributes.**
34
+ An HTML attribute is always a string, so passing `messages`, `models`, `context`,
35
+ `suggestions`, or `slashCommands` as an attribute silently fails.
36
+
37
+ ```js
38
+ const chat = document.querySelector('kitn-chat');
39
+ chat.messages = [{ id: '1', role: 'assistant', content: 'Hi!' }]; // ✅ property
40
+ ```
41
+
42
+ ```html
43
+ <kitn-chat messages="[...]"></kitn-chat> <!-- ❌ never works -->
44
+ ```
45
+
46
+ Only scalar props (string / number / boolean) work as attributes — for example
47
+ `placeholder`, `loading`, and `theme`.
48
+
49
+ ## How agents consume these files
50
+
51
+ | Tool | How it finds them |
52
+ |---|---|
53
+ | Claude Code | Reference `@llms.txt` in `CLAUDE.md`, or read `node_modules/@kitnai/chat/llms-full.txt` |
54
+ | GitHub Copilot | Workspace indexing picks up the file in `node_modules`; reference it from `.github/copilot-instructions.md` |
55
+ | Cursor | Point `.cursorrules` at the installed `llms.txt` |
56
+ | Codex / ChatGPT | Fetch https://kitn.dev/llms.txt directly |
57
+ | Any agent | `npm install @kitnai/chat` → file is at `node_modules/@kitnai/chat/llms.txt` |
58
+
59
+ ## Build a chat app (the runbook agents follow)
60
+
61
+ ```js
62
+ import '@kitnai/chat/elements';
63
+ const chat = document.querySelector('kitn-chat');
64
+ chat.messages = [];
65
+
66
+ chat.addEventListener('submit', async (e) => {
67
+ const userText = e.detail.value;
68
+
69
+ // Append the user message (new array)
70
+ const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: userText }];
71
+ chat.messages = history;
72
+ chat.loading = true;
73
+
74
+ // Empty assistant placeholder to stream into
75
+ const aid = crypto.randomUUID();
76
+ chat.messages = [...history, { id: aid, role: 'assistant', content: '' }];
77
+
78
+ // Stream — reassign a NEW array with a NEW message object each chunk.
79
+ // Mutating in place will NOT re-render.
80
+ let answer = '';
81
+ for await (const token of streamFromYourAPI(history)) {
82
+ answer += token;
83
+ chat.messages = chat.messages.map((m) => (m.id === aid ? { ...m, content: answer } : m));
84
+ }
85
+ chat.loading = false;
86
+ });
87
+ ```
88
+
89
+ ## Links
90
+
91
+ - <a href="/llms.txt" target="_blank" rel="noreferrer"><code>llms.txt</code></a> — orientation (also published at `https://kitn.dev/llms.txt`)
92
+ - <a href="/llms-full.txt" target="_blank" rel="noreferrer"><code>llms-full.txt</code></a> — full per-element reference (also `https://kitn.dev/llms-full.txt`)
93
+ - Custom Elements Manifest — `dist/custom-elements.json` (published at `https://unpkg.com/@kitnai/chat/dist/custom-elements.json`)
@@ -1,7 +1,7 @@
1
1
  import { Meta, Canvas } from '@storybook/addon-docs/blocks';
2
2
  import * as FullChat from '../full-chat.stories';
3
3
 
4
- <Meta title="Getting Started" />
4
+ <Meta title="Docs/Getting Started" />
5
5
 
6
6
  # Getting Started
7
7
 
@@ -73,4 +73,4 @@ Everything assembled into a real app — a conversation sidebar, a message threa
73
73
 
74
74
  Open it full-screen under **Examples → Full Chat App**, then explore each building block on its own page in the sidebar.
75
75
 
76
- Ready to make it yours? See **[Theming](?path=/docs/theming--docs)**. Wiring a real model? See **[Integrations](?path=/docs/integrations--docs)**.
76
+ Ready to make it yours? See **[Theming](?path=/docs/docs-theming--docs)**. Wiring a real model? See **[Integrations](?path=/docs/docs-frameworks-integrations--docs)**.
@@ -1,6 +1,6 @@
1
1
  import { Meta } from '@storybook/addon-docs/blocks';
2
2
 
3
- <Meta title="Installation" />
3
+ <Meta title="Docs/Installation" />
4
4
 
5
5
  # Installation
6
6
 
@@ -45,4 +45,4 @@ npm run build # emits dist/kitn-chat.es.js
45
45
  - **SolidJS is bundled in** — the host page needs nothing else.
46
46
  - The kit's CSS is injected into each element's Shadow DOM automatically; importing `theme.css` is optional and only needed if you want to override design tokens.
47
47
 
48
- Head to **[Getting Started](?path=/docs/getting-started--docs)** to render your first chat.
48
+ Head to **[Getting Started](?path=/docs/docs-getting-started--docs)** to render your first chat.
@@ -1,37 +1,436 @@
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
+ ```html
34
+ <!DOCTYPE html>
35
+ <html>
36
+ <head>
37
+ <!-- optional: only needed to rebrand host-page markup -->
38
+ <link rel="stylesheet" href="./node_modules/@kitnai/chat/dist/theme.tokens.css" />
39
+ </head>
40
+ <body style="height: 100vh; margin: 0;">
41
+ <kitn-chat id="chat" style="display: block; height: 100%;"></kitn-chat>
42
+
43
+ <script type="module">
44
+ import '@kitnai/chat/elements';
45
+
46
+ const chat = document.getElementById('chat');
47
+
48
+ // Arrays and objects → JS properties
49
+ chat.messages = [
50
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?', actions: ['copy', 'like', 'dislike'] },
51
+ ];
52
+ chat.suggestions = ['Summarize the chat', 'Start fresh'];
53
+
54
+ // Events → addEventListener on the element (they don't bubble)
55
+ chat.addEventListener('submit', async (e) => {
56
+ const text = e.detail.value;
57
+
58
+ // Append user message (new array → triggers re-render)
59
+ const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
60
+ chat.messages = history;
61
+ chat.loading = true;
62
+
63
+ // Stream into an empty assistant placeholder
64
+ const aid = crypto.randomUUID();
65
+ chat.messages = [...history, { id: aid, role: 'assistant', content: '' }];
66
+
67
+ let answer = '';
68
+ for await (const token of streamFromYourAPI(history)) {
69
+ answer += token;
70
+ // Replace the placeholder with a new object each chunk
71
+ chat.messages = chat.messages.map((m) =>
72
+ m.id === aid ? { ...m, content: answer } : m
73
+ );
74
+ }
75
+ chat.loading = false;
76
+ });
77
+ </script>
78
+ </body>
79
+ </html>
80
+ ```
81
+
82
+ > **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.
83
+
84
+ ### Scalar attributes
85
+
86
+ Scalars can go directly in the HTML as attributes:
87
+
88
+ ```html
89
+ <!-- theme, placeholder, and loading are scalar → safe as attributes -->
90
+ <kitn-chat
91
+ theme="dark"
92
+ placeholder="Ask anything…"
93
+ ></kitn-chat>
94
+ ```
95
+
96
+ ---
97
+
98
+ ## React
99
+
100
+ 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.
101
+
102
+ ```tsx
103
+ import { KitnChat, KitnConversationList } from '@kitnai/chat/react';
104
+ import { useState } from 'react';
105
+
106
+ type Message = {
107
+ id: string;
108
+ role: 'user' | 'assistant';
109
+ content: string;
110
+ actions?: string[];
111
+ };
112
+
113
+ export function App() {
114
+ const [messages, setMessages] = useState<Message[]>([
115
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?', actions: ['copy'] },
116
+ ]);
117
+
118
+ const handleSubmit = async (e: CustomEvent<{ value: string }>) => {
119
+ const text = e.detail.value;
120
+
121
+ const history = [...messages, { id: crypto.randomUUID(), role: 'user' as const, content: text }];
122
+ setMessages(history);
123
+
124
+ // Placeholder to stream into
125
+ const aid = crypto.randomUUID();
126
+ setMessages([...history, { id: aid, role: 'assistant', content: '' }]);
127
+
128
+ let answer = '';
129
+ for await (const token of streamFromYourAPI(history)) {
130
+ answer += token;
131
+ setMessages((prev) =>
132
+ prev.map((m) => (m.id === aid ? { ...m, content: answer } : m))
133
+ );
134
+ }
135
+ };
136
+
137
+ return (
138
+ <KitnChat
139
+ messages={messages}
140
+ suggestions={['Summarize the chat', 'Start fresh']}
141
+ onSubmit={handleSubmit}
142
+ onMessageaction={(e) => console.log('action', e.detail)}
143
+ style={{ display: 'block', height: '100vh' }}
144
+ />
145
+ );
146
+ }
147
+ ```
148
+
149
+ ### Event prop naming
150
+
151
+ Event props follow the `on` + event-name pattern (camelCased):
152
+
153
+ | DOM event name | React prop |
154
+ |---|---|
155
+ | `submit` | `onSubmit` |
156
+ | `valuechange` | `onValuechange` |
157
+ | `messageaction` | `onMessageaction` |
158
+ | `modelchange` | `onModelchange` |
159
+ | `suggestionclick` | `onSuggestionclick` |
160
+ | `slashselect` | `onSlashselect` |
161
+ | `search` | `onSearch` |
162
+ | `voice` | `onVoice` |
163
+
164
+ ### All 27 elements have typed wrappers
165
+
166
+ Component names are the PascalCase of the tag name:
167
+
168
+ ```tsx
169
+ import {
170
+ KitnChat,
171
+ KitnConversationList,
172
+ KitnPromptInput,
173
+ KitnMessage,
174
+ KitnMarkdown,
175
+ KitnCodeBlock,
176
+ KitnReasoning,
177
+ KitnTool,
178
+ KitnContextMeter,
179
+ KitnModelSwitcher,
180
+ KitnAttachments,
181
+ KitnLoader,
182
+ // …all 27 elements
183
+ } from '@kitnai/chat/react';
184
+ ```
185
+
186
+ ### Without the React wrappers (raw custom element)
187
+
188
+ 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`:
189
+
190
+ ```tsx
191
+ import { useEffect, useRef, useState } from 'react';
192
+ import '@kitnai/chat/elements';
193
+
194
+ export function Chat() {
195
+ const chatRef = useRef<HTMLElement>(null);
196
+ const [messages, setMessages] = useState([
197
+ { id: '1', role: 'assistant', content: 'Hello!' },
198
+ ]);
199
+
200
+ // Set object/array properties — cannot go through JSX props
201
+ useEffect(() => {
202
+ const el = chatRef.current;
203
+ if (!el) return;
204
+ (el as any).messages = messages;
205
+ }, [messages]);
206
+
207
+ // Wire events with addEventListener
208
+ useEffect(() => {
209
+ const el = chatRef.current;
210
+ if (!el) return;
211
+
212
+ const onSubmit = (e: Event) => {
213
+ const { value } = (e as CustomEvent<{ value: string }>).detail;
214
+ setMessages((prev) => [
215
+ ...prev,
216
+ { id: crypto.randomUUID(), role: 'user', content: value },
217
+ ]);
218
+ };
219
+
220
+ el.addEventListener('submit', onSubmit);
221
+ return () => el.removeEventListener('submit', onSubmit);
222
+ }, []);
223
+
224
+ return <kitn-chat ref={chatRef} style={{ display: 'block', height: '100vh' }} />;
225
+ }
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Vue
231
+
232
+ 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.
233
+
234
+ ```html
235
+ <script setup lang="ts">
236
+ import { ref, onMounted } from 'vue';
237
+ import '@kitnai/chat/elements';
238
+
239
+ const messages = ref([
240
+ { id: '1', role: 'assistant', content: 'Hello! How can I help?' },
241
+ ]);
242
+
243
+ const handleSubmit = (e: CustomEvent<{ value: string }>) => {
244
+ const text = e.detail.value;
245
+ messages.value = [
246
+ ...messages.value,
247
+ { id: crypto.randomUUID(), role: 'user', content: text },
248
+ ];
249
+ // …stream a reply and append an assistant message
250
+ };
251
+ </script>
252
+
253
+ <template>
254
+ <!-- Arrays/objects → .prop modifier; scalars → plain attributes -->
255
+ <kitn-chat
256
+ :messages.prop="messages"
257
+ placeholder="Ask anything…"
258
+ theme="auto"
259
+ style="display: block; height: 100vh"
260
+ @submit="handleSubmit"
261
+ />
262
+ </template>
263
+ ```
264
+
265
+ ### Vue + TypeScript: augmenting JSX types
266
+
267
+ Add `@kitnai/chat/elements` to your `vite.config.ts` / `env.d.ts` once so Vue's template compiler knows the element's attributes:
268
+
269
+ ```ts
270
+ // env.d.ts (or vite-env.d.ts)
271
+ /// <reference types="@kitnai/chat/elements" />
272
+ ```
273
+
274
+ ### Sidebar + chat together (Vue)
275
+
276
+ 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>`:
277
+
278
+ ```html
279
+ <script setup lang="ts">
280
+ import '@kitnai/chat/elements';
281
+ import { ref } from 'vue';
282
+
283
+ const conversations = ref([
284
+ { id: 'c1', title: 'First chat', scope: { type: 'document' }, messageCount: 3,
285
+ lastMessageAt: '2026-06-01T12:00:00Z', updatedAt: '2026-06-01T12:00:00Z' },
286
+ ]);
287
+ const activeId = ref('c1');
288
+ const messages = ref([{ id: '1', role: 'assistant', content: 'Hi!' }]);
289
+
290
+ const onSelect = (e: CustomEvent<{ id: string }>) => {
291
+ activeId.value = e.detail.id;
292
+ // load messages for the selected conversation
293
+ };
294
+ </script>
295
+
296
+ <template>
297
+ <div style="display: flex; height: 100vh;">
298
+ <kitn-conversation-list
299
+ :conversations.prop="conversations"
300
+ :active-id="activeId"
301
+ style="width: 260px"
302
+ @select="onSelect"
303
+ />
304
+ <kitn-chat
305
+ :messages.prop="messages"
306
+ style="flex: 1"
307
+ />
308
+ </div>
309
+ </template>
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Svelte
315
+
316
+ Svelte's template compiler sets DOM **properties** when you bind with `bind:` or use the `|propname` directive. For custom events, use `on:eventname`:
317
+
318
+ ```html
319
+ <script>
320
+ import '@kitnai/chat/elements';
321
+
322
+ let messages = [
323
+ { id: '1', role: 'assistant', content: 'Hello!' }
324
+ ];
325
+
326
+ function handleSubmit(e) {
327
+ const text = e.detail.value;
328
+ messages = [...messages, { id: crypto.randomUUID(), role: 'user', content: text }];
329
+ // …stream reply
330
+ }
331
+ </script>
332
+
333
+ <!-- use:action pattern to set properties -->
334
+ <kitn-chat
335
+ use:setProps={{ messages }}
336
+ style="display: block; height: 100vh"
337
+ on:submit={handleSubmit}
338
+ />
339
+
340
+ <script context="module">
341
+ function setProps(node, props) {
342
+ Object.assign(node, props);
343
+ return {
344
+ update(newProps) { Object.assign(node, newProps); }
345
+ };
346
+ }
347
+ </script>
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Angular
353
+
354
+ 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`).
355
+
356
+ ```ts
357
+ // main.ts — register the kitn-* elements once, app-wide
358
+ import '@kitnai/chat/elements';
359
+ ```
360
+
361
+ ```ts
362
+ // app.component.ts
363
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
364
+
365
+ @Component({
366
+ selector: 'app-root',
367
+ standalone: true,
368
+ templateUrl: './app.component.html',
369
+ schemas: [CUSTOM_ELEMENTS_SCHEMA], // allow the <kitn-*> custom elements
370
+ })
371
+ export class AppComponent {
372
+ messages = [{ id: '1', role: 'assistant', content: 'Hello!' }];
373
+ models = [{ id: 'opus', name: 'Claude Opus' }];
374
+ loading = false;
375
+
376
+ onSubmit(e: Event) {
377
+ const { value } = (e as CustomEvent<{ value: string }>).detail;
378
+ // Reassign a NEW array so change detection re-renders.
379
+ this.messages = [
380
+ ...this.messages,
381
+ { id: crypto.randomUUID(), role: 'user', content: value },
382
+ ];
383
+ // …stream the reply, reassigning this.messages with a new array each chunk
384
+ }
385
+
386
+ onModelChange(e: Event) {
387
+ console.log('model:', (e as CustomEvent<{ modelId: string }>).detail.modelId);
388
+ }
389
+ }
390
+ ```
391
+
392
+ ```html
393
+ <!-- app.component.html -->
394
+ <kitn-chat
395
+ [messages]="messages"
396
+ [models]="models"
397
+ [loading]="loading"
398
+ theme="auto"
399
+ style="display: block; height: 100vh"
400
+ (submit)="onSubmit($event)"
401
+ (modelchange)="onModelChange($event)"
402
+ ></kitn-chat>
403
+ ```
404
+
405
+ > **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.
406
+
407
+ ---
8
408
 
9
409
  ## Streaming from OpenRouter
10
410
 
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.
411
+ [OpenRouter](https://openrouter.ai) exposes an OpenAI-compatible streaming API (Server-Sent Events). Wire it into the `submit` event:
12
412
 
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.
413
+ > **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
414
 
15
415
  ```js
16
416
  chat.addEventListener('submit', async (e) => {
17
417
  const text = e.detail.value.trim();
18
418
  if (!text) return;
19
419
 
20
- // 1. Show the user message; clear the input
420
+ // 1. Show the user message
21
421
  const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
22
422
  chat.messages = history;
23
- chat.value = '';
24
423
  chat.loading = true;
25
424
 
26
- // 2. Add an empty assistant message to stream into
425
+ // 2. Empty assistant placeholder to stream into
27
426
  const assistantId = crypto.randomUUID();
28
427
  chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];
29
428
 
30
- // In production, replace this URL with your own proxy endpoint.
429
+ // In production, replace this with your own proxy endpoint.
31
430
  const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
32
431
  method: 'POST',
33
432
  headers: {
34
- 'Authorization': `Bearer ${OPENROUTER_API_KEY}`, // server-side in production!
433
+ 'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
35
434
  'Content-Type': 'application/json',
36
435
  },
37
436
  body: JSON.stringify({
@@ -52,17 +451,16 @@ chat.addEventListener('submit', async (e) => {
52
451
  buffer += decoder.decode(value, { stream: true });
53
452
 
54
453
  const lines = buffer.split('\n');
55
- buffer = lines.pop(); // keep the partial last line
454
+ buffer = lines.pop();
56
455
  for (const line of lines) {
57
456
  const s = line.trim();
58
- if (!s.startsWith('data:')) continue; // skip keep-alive comments
457
+ if (!s.startsWith('data:')) continue;
59
458
  const payload = s.slice(5).trim();
60
459
  if (payload === '[DONE]') continue;
61
460
  try {
62
461
  const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
63
462
  if (!delta) continue;
64
463
  answer += delta;
65
- // New object for the streaming message so the row re-renders
66
464
  chat.messages = chat.messages.map((m) =>
67
465
  m.id === assistantId ? { ...m, content: answer } : m
68
466
  );
@@ -73,6 +471,8 @@ chat.addEventListener('submit', async (e) => {
73
471
  });
74
472
  ```
75
473
 
474
+ ---
475
+
76
476
  ## Text-to-speech (TTS)
77
477
 
78
478
  ### Browser-native (zero dependencies)
@@ -91,7 +491,7 @@ function speak(text) {
91
491
 
92
492
  ### Cloud TTS (OpenAI, ElevenLabs, …)
93
493
 
94
- For higher-quality voices, have your backend call a TTS API and return audio, then play it (keep the provider key server-side):
494
+ For higher-quality voices, have your backend call a TTS API and return audio (keep the provider key server-side):
95
495
 
96
496
  ```js
97
497
  async function speakCloud(text) {
@@ -107,4 +507,4 @@ async function speakCloud(text) {
107
507
 
108
508
  ## Speech-to-text
109
509
 
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.
510
+ 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,7 +1,7 @@
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
 
@@ -21,9 +21,9 @@ Message threads, prompt inputs, streaming responses, markdown + code rendering,
21
21
 
22
22
  ## Where to next
23
23
 
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
24
+ - **[Installation](?path=/docs/docs-installation--docs)** — add it to your project
25
+ - **[Getting Started](?path=/docs/docs-getting-started--docs)** — your first chat in a few lines, plus a full example
26
+ - **[Theming](?path=/docs/docs-theming--docs)** — make it match your brand
27
+ - **[Integrations](?path=/docs/docs-frameworks-integrations--docs)** — stream responses from OpenRouter and add text-to-speech
28
28
 
29
29
  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