@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.
- package/README.md +11 -0
- package/dist/custom-elements.json +2494 -0
- package/dist/kitn-chat.es.js +52 -39
- package/dist/llms/llms-full.txt +667 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/theme.tokens.css +133 -0
- package/frameworks/react/index.tsx +530 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +667 -0
- package/llms.txt +104 -0
- package/package.json +34 -5
- 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/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 +15 -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.tsx +51 -7
- 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 +102 -13
- package/src/elements/element-types.d.ts +404 -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-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 +31 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/source.tsx +67 -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 +2 -2
- 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 +2 -2
- package/src/stories/docs/Integrations.mdx +415 -15
- package/src/stories/docs/Introduction.mdx +5 -5
- package/src/stories/docs/Theming.mdx +1 -1
- package/src/stories/typography.stories.tsx +78 -0
- package/src/ui/button.tsx +1 -1
- package/src/ui/collapsible.tsx +119 -8
- package/src/ui/dropdown.tsx +177 -12
- package/src/ui/hover-card.tsx +147 -26
- package/src/ui/overlay.tsx +151 -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 +72 -43
- 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
|
-
|
|
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).
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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}`,
|
|
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();
|
|
454
|
+
buffer = lines.pop();
|
|
56
455
|
for (const line of lines) {
|
|
57
456
|
const s = line.trim();
|
|
58
|
-
if (!s.startsWith('data:')) continue;
|
|
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
|
|
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
|
|
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.
|