@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.
- package/README.md +35 -5
- package/dist/custom-elements.json +2969 -0
- package/dist/kitn-chat.es.js +52 -39
- package/dist/llms/llms-full.txt +718 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/theme.tokens.css +137 -0
- package/frameworks/react/index.tsx +584 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +718 -0
- package/llms.txt +104 -0
- package/package.json +53 -6
- package/src/components/attachments.tsx +4 -2
- package/src/components/chain-of-thought.tsx +1 -1
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +217 -0
- package/src/components/checkpoint.tsx +7 -3
- package/src/components/context.tsx +14 -18
- package/src/components/conversation-item.tsx +1 -1
- package/src/components/conversation-list.tsx +5 -4
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +1 -0
- package/src/components/model-switcher.tsx +3 -3
- package/src/components/prompt-input.tsx +20 -2
- package/src/components/reasoning.tsx +2 -2
- package/src/components/scroll-button.tsx +1 -0
- package/src/components/slash-command.tsx +17 -8
- package/src/components/source.tsx +2 -2
- package/src/components/thinking-bar.tsx +2 -2
- package/src/components/tool.tsx +17 -6
- package/src/components/voice-input.tsx +5 -1
- package/src/elements/attachments.tsx +132 -0
- package/src/elements/chain-of-thought.tsx +45 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-workspace.tsx +122 -0
- package/src/elements/chat.tsx +31 -228
- package/src/elements/checkpoint.tsx +43 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/context-meter.tsx +71 -0
- package/src/elements/conversation-list.tsx +6 -0
- package/src/elements/default-input.tsx +22 -1
- package/src/elements/define.tsx +98 -12
- package/src/elements/element-types.d.ts +444 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.tsx +33 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/kitn-attachments.stories.tsx +181 -0
- package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
- package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
- package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
- package/src/elements/kitn-checkpoint.stories.tsx +71 -0
- package/src/elements/kitn-code-block.stories.tsx +82 -0
- package/src/elements/kitn-context-meter.stories.tsx +85 -0
- package/src/elements/kitn-empty.stories.tsx +110 -0
- package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
- package/src/elements/kitn-file-upload.stories.tsx +81 -0
- package/src/elements/kitn-image.stories.tsx +70 -0
- package/src/elements/kitn-loader.stories.tsx +87 -0
- package/src/elements/kitn-markdown.stories.tsx +75 -0
- package/src/elements/kitn-message-skills.stories.tsx +74 -0
- package/src/elements/kitn-message.stories.tsx +105 -0
- package/src/elements/kitn-model-switcher.stories.tsx +80 -0
- package/src/elements/kitn-prompt-input.stories.tsx +74 -16
- package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
- package/src/elements/kitn-reasoning.stories.tsx +76 -0
- package/src/elements/kitn-response-stream.stories.tsx +79 -0
- package/src/elements/kitn-source-list.stories.tsx +77 -0
- package/src/elements/kitn-source.stories.tsx +87 -0
- package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
- package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
- package/src/elements/kitn-tool.stories.tsx +88 -0
- package/src/elements/kitn-voice-input.stories.tsx +87 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.tsx +22 -0
- package/src/elements/message.tsx +125 -0
- package/src/elements/model-switcher.tsx +35 -0
- package/src/elements/prompt-input.tsx +83 -7
- package/src/elements/prompt-suggestions.tsx +58 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +32 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/source.tsx +67 -0
- package/src/elements/styles.css +14 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +0 -1
- package/src/primitives/chat-config.tsx +3 -3
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ForAIAgents.mdx +93 -0
- package/src/stories/docs/GettingStarted.mdx +2 -2
- package/src/stories/docs/Installation.mdx +29 -2
- package/src/stories/docs/Integrations.mdx +417 -15
- package/src/stories/docs/Introduction.mdx +17 -8
- package/src/stories/docs/Theming.mdx +1 -1
- package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
- package/src/stories/pattern-docked-widget.stories.tsx +93 -0
- package/src/stories/pattern-empty-state.stories.tsx +76 -0
- package/src/stories/typography.stories.tsx +78 -0
- package/src/ui/button.tsx +1 -1
- package/src/ui/collapsible.stories.tsx +70 -0
- package/src/ui/collapsible.tsx +119 -8
- package/src/ui/dropdown.stories.tsx +60 -0
- package/src/ui/dropdown.tsx +177 -12
- package/src/ui/hover-card.stories.tsx +78 -0
- package/src/ui/hover-card.tsx +147 -26
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +151 -0
- package/src/ui/scroll-area.stories.tsx +51 -0
- package/src/ui/textarea.stories.tsx +77 -0
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/tooltip.stories.tsx +1 -1
- package/src/ui/tooltip.tsx +59 -13
- package/src/utils/cn.ts +19 -1
- package/theme.css +76 -43
- 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
|
-
|
|
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).
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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}`,
|
|
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();
|
|
456
|
+
buffer = lines.pop();
|
|
56
457
|
for (const line of lines) {
|
|
57
458
|
const s = line.trim();
|
|
58
|
-
if (!s.startsWith('data:')) continue;
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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
|
-
- **
|
|
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
|
|
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.
|
|
@@ -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
|
+
};
|