@kitnai/chat 0.1.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/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/bash-InADTalH.js +6 -0
- package/dist/core-AYMC6_lb.js +5874 -0
- package/dist/engine-javascript-vq0WuIJl.js +2643 -0
- package/dist/github-dark-dimmed-DUshB20C.js +4 -0
- package/dist/github-light-JYsPkUQd.js +4 -0
- package/dist/javascript-C25yR2R2.js +6 -0
- package/dist/json-DxJze_jm.js +6 -0
- package/dist/kitn-chat.es.js +6632 -0
- package/dist/tsx-B8rCNbgL.js +6 -0
- package/dist/typescript-RycA9KXf.js +6 -0
- package/package.json +80 -0
- package/src/components/attachments.stories.tsx +304 -0
- package/src/components/attachments.tsx +394 -0
- package/src/components/chain-of-thought.stories.tsx +212 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +188 -0
- package/src/components/chat-container.tsx +78 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/checkpoint.stories.tsx +103 -0
- package/src/components/checkpoint.tsx +81 -0
- package/src/components/code-block.stories.tsx +151 -0
- package/src/components/code-block.tsx +99 -0
- package/src/components/context.stories.tsx +180 -0
- package/src/components/context.tsx +323 -0
- package/src/components/conversation-item.stories.tsx +126 -0
- package/src/components/conversation-item.tsx +18 -0
- package/src/components/conversation-list.stories.tsx +134 -0
- package/src/components/conversation-list.tsx +100 -0
- package/src/components/empty.stories.tsx +435 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +101 -0
- package/src/components/feedback-bar.tsx +58 -0
- package/src/components/file-upload.stories.tsx +157 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/image.stories.tsx +90 -0
- package/src/components/image.tsx +67 -0
- package/src/components/loader.stories.tsx +182 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +181 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-narrow.stories.tsx +330 -0
- package/src/components/message-skills.stories.tsx +212 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +282 -0
- package/src/components/message.tsx +149 -0
- package/src/components/model-switcher.stories.tsx +98 -0
- package/src/components/model-switcher.tsx +36 -0
- package/src/components/prompt-input.stories.tsx +223 -0
- package/src/components/prompt-input.tsx +190 -0
- package/src/components/prompt-suggestion.stories.tsx +143 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +141 -0
- package/src/components/reasoning.tsx +157 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +101 -0
- package/src/components/scroll-button.tsx +33 -0
- package/src/components/slash-command.stories.tsx +164 -0
- package/src/components/slash-command.tsx +223 -0
- package/src/components/source.stories.tsx +125 -0
- package/src/components/source.tsx +129 -0
- package/src/components/text-shimmer.stories.tsx +88 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +88 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +154 -0
- package/src/components/tool.tsx +173 -0
- package/src/components/voice-input.stories.tsx +84 -0
- package/src/components/voice-input.tsx +103 -0
- package/src/elements/chat-types.ts +14 -0
- package/src/elements/chat.tsx +111 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/conversation-list.tsx +26 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +53 -0
- package/src/elements/define.tsx +54 -0
- package/src/elements/kitn-chat.stories.tsx +105 -0
- package/src/elements/kitn-conversation-list.stories.tsx +177 -0
- package/src/elements/kitn-prompt-input.stories.tsx +123 -0
- package/src/elements/prompt-input.tsx +39 -0
- package/src/elements/register.ts +9 -0
- package/src/elements/styles.css +12 -0
- package/src/index.ts +128 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/highlighter.ts +150 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-stick-to-bottom.ts +43 -0
- package/src/primitives/use-text-stream.ts +112 -0
- package/src/primitives/use-voice-recorder.ts +50 -0
- package/src/stories/chat-panel-layout.stories.tsx +144 -0
- package/src/stories/chat-scene.tsx +570 -0
- package/src/stories/checkpoint-restore.stories.tsx +224 -0
- package/src/stories/context-usage.stories.tsx +155 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +151 -0
- package/src/stories/conversation-with-sources.stories.tsx +165 -0
- package/src/stories/docs/GettingStarted.mdx +76 -0
- package/src/stories/docs/Installation.mdx +48 -0
- package/src/stories/docs/Integrations.mdx +110 -0
- package/src/stories/docs/Introduction.mdx +29 -0
- package/src/stories/docs/Theming.mdx +87 -0
- package/src/stories/docs/theme-editor/canvas.tsx +32 -0
- package/src/stories/docs/theme-editor/inspector.tsx +66 -0
- package/src/stories/docs/theme-editor/presets.test.ts +32 -0
- package/src/stories/docs/theme-editor/presets.ts +64 -0
- package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
- package/src/stories/docs/theme-editor/theme-css.ts +15 -0
- package/src/stories/docs/theme-editor/theme-editor.tsx +145 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +167 -0
- package/src/stories/prompt-input-variants.stories.tsx +179 -0
- package/src/stories/streaming-response.stories.tsx +234 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/types.ts +41 -0
- package/src/ui/avatar.stories.tsx +104 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +87 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +146 -0
- package/src/ui/button.tsx +37 -0
- package/src/ui/collapsible.tsx +14 -0
- package/src/ui/dialog.tsx +21 -0
- package/src/ui/dropdown.tsx +26 -0
- package/src/ui/hover-card.tsx +48 -0
- package/src/ui/resizable.stories.tsx +171 -0
- package/src/ui/resizable.tsx +219 -0
- package/src/ui/scroll-area.tsx +13 -0
- package/src/ui/separator.stories.tsx +82 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +338 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +75 -0
- package/src/ui/tooltip.tsx +22 -0
- package/src/utils/cn.ts +6 -0
- package/theme.css +115 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import {
|
|
3
|
+
ChatContainer, ChatContainerContent, ChatContainerScrollAnchor,
|
|
4
|
+
Message, MessageAvatar, MessageContent, MessageActions,
|
|
5
|
+
PromptInput, PromptInputTextarea, PromptInputActions,
|
|
6
|
+
Source, SourceTrigger, SourceContent, SourceList,
|
|
7
|
+
Button, Separator,
|
|
8
|
+
} from '../index';
|
|
9
|
+
import { Copy, ThumbsUp, ThumbsDown, ArrowUp } from 'lucide-solid';
|
|
10
|
+
|
|
11
|
+
const meta: Meta = {
|
|
12
|
+
title: 'Examples/Conversation with Sources',
|
|
13
|
+
parameters: { layout: 'padded' },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj;
|
|
18
|
+
|
|
19
|
+
export const Default: Story = {
|
|
20
|
+
render: () => (
|
|
21
|
+
<div class="flex flex-col h-[650px] w-full max-w-2xl bg-background rounded-xl shadow-lg overflow-hidden">
|
|
22
|
+
<div class="flex items-center px-4 py-3">
|
|
23
|
+
<h2 class="text-sm font-semibold text-foreground">Research: WebAssembly in Production</h2>
|
|
24
|
+
</div>
|
|
25
|
+
<Separator />
|
|
26
|
+
|
|
27
|
+
<ChatContainer class="flex-1 p-4">
|
|
28
|
+
<ChatContainerContent class="space-y-6 py-4">
|
|
29
|
+
|
|
30
|
+
{/* First exchange */}
|
|
31
|
+
<Message>
|
|
32
|
+
<MessageAvatar src="" fallback="U" alt="User" />
|
|
33
|
+
<MessageContent>
|
|
34
|
+
What are the real-world performance benefits of using WebAssembly in production web apps? I want concrete examples, not just benchmarks.
|
|
35
|
+
</MessageContent>
|
|
36
|
+
</Message>
|
|
37
|
+
|
|
38
|
+
<Message>
|
|
39
|
+
<MessageAvatar src="" fallback="AI" alt="Assistant" />
|
|
40
|
+
<div class="flex-1 space-y-2">
|
|
41
|
+
<MessageContent markdown>
|
|
42
|
+
{`Several major companies have deployed WebAssembly in production with measurable results:
|
|
43
|
+
|
|
44
|
+
**Figma** replaced their C++ to asm.js pipeline with Wasm and saw a **3x performance improvement** in load times. Their design tool renders complex vector graphics at 60fps using a Wasm-compiled engine.
|
|
45
|
+
|
|
46
|
+
**Google Earth** ported their native rendering engine to Wasm, enabling the full 3D globe experience in browsers without plugins. Frame rendering dropped from **40ms to 12ms** per frame.
|
|
47
|
+
|
|
48
|
+
**Shopify** uses Wasm for their theme editor's Liquid template parsing, achieving **10x faster** template compilation compared to their previous JavaScript parser.
|
|
49
|
+
|
|
50
|
+
The pattern is consistent: CPU-intensive tasks like **image processing, cryptography, physics simulation, and codec operations** see the largest gains. IO-bound operations don't benefit as much.`}
|
|
51
|
+
</MessageContent>
|
|
52
|
+
|
|
53
|
+
<SourceList>
|
|
54
|
+
<Source href="https://www.figma.com/blog/webassembly-cut-figmas-load-time-by-3x/">
|
|
55
|
+
<SourceTrigger label={1} />
|
|
56
|
+
<SourceContent
|
|
57
|
+
title="WebAssembly cut Figma's load time by 3x"
|
|
58
|
+
description="How Figma leveraged WebAssembly to dramatically improve their browser-based design tool performance."
|
|
59
|
+
/>
|
|
60
|
+
</Source>
|
|
61
|
+
<Source href="https://web.dev/case-studies/earth-webassembly">
|
|
62
|
+
<SourceTrigger label={2} />
|
|
63
|
+
<SourceContent
|
|
64
|
+
title="Google Earth and WebAssembly - web.dev"
|
|
65
|
+
description="Case study on porting Google Earth's C++ rendering engine to WebAssembly for browser delivery."
|
|
66
|
+
/>
|
|
67
|
+
</Source>
|
|
68
|
+
<Source href="https://shopify.engineering/shopify-webassembly">
|
|
69
|
+
<SourceTrigger label={3} />
|
|
70
|
+
<SourceContent
|
|
71
|
+
title="How Shopify Uses WebAssembly"
|
|
72
|
+
description="Shopify's journey using WebAssembly for Liquid template parsing in their online store editor."
|
|
73
|
+
/>
|
|
74
|
+
</Source>
|
|
75
|
+
</SourceList>
|
|
76
|
+
|
|
77
|
+
<MessageActions>
|
|
78
|
+
<Button variant="ghost" size="icon-sm"><Copy class="size-3.5" /></Button>
|
|
79
|
+
<Button variant="ghost" size="icon-sm"><ThumbsUp class="size-3.5" /></Button>
|
|
80
|
+
<Button variant="ghost" size="icon-sm"><ThumbsDown class="size-3.5" /></Button>
|
|
81
|
+
</MessageActions>
|
|
82
|
+
</div>
|
|
83
|
+
</Message>
|
|
84
|
+
|
|
85
|
+
{/* Second exchange with different citation pattern */}
|
|
86
|
+
<Message>
|
|
87
|
+
<MessageAvatar src="" fallback="U" alt="User" />
|
|
88
|
+
<MessageContent>
|
|
89
|
+
What about the downsides? Are there situations where Wasm actually hurts performance?
|
|
90
|
+
</MessageContent>
|
|
91
|
+
</Message>
|
|
92
|
+
|
|
93
|
+
<Message>
|
|
94
|
+
<MessageAvatar src="" fallback="AI" alt="Assistant" />
|
|
95
|
+
<div class="flex-1 space-y-2">
|
|
96
|
+
<MessageContent markdown>
|
|
97
|
+
{`Yes, there are real tradeoffs to consider:
|
|
98
|
+
|
|
99
|
+
**Bundle size** -- Wasm modules can be significantly larger than equivalent JavaScript. The Squoosh image editor found their Wasm codecs added **2-4MB** of download size, which they mitigated with lazy loading.
|
|
100
|
+
|
|
101
|
+
**DOM access overhead** -- Every call from Wasm to the DOM crosses a boundary with serialization cost. Applications that frequently manipulate the DOM (like React-style UI frameworks) can actually be **slower** in Wasm than optimized JavaScript.
|
|
102
|
+
|
|
103
|
+
**Startup latency** -- Wasm modules must be compiled before execution. While streaming compilation helps, large modules can take **200-500ms** to initialize on mobile devices.
|
|
104
|
+
|
|
105
|
+
**Garbage collection** -- Until the Wasm GC proposal ships broadly, languages like Go and Java bring their own GC runtime, adding **5-20MB** to module size.
|
|
106
|
+
|
|
107
|
+
The consensus from the Chrome team's analysis is: **use Wasm for compute-heavy inner loops, keep UI and orchestration in JavaScript**.`}
|
|
108
|
+
</MessageContent>
|
|
109
|
+
|
|
110
|
+
<SourceList>
|
|
111
|
+
<Source href="https://web.dev/squoosh-v2/">
|
|
112
|
+
<SourceTrigger label={1} showFavicon />
|
|
113
|
+
<SourceContent
|
|
114
|
+
title="Squoosh v2: codec improvements with WebAssembly"
|
|
115
|
+
description="How the Squoosh team manages Wasm codec size and loading strategies for their image compression tool."
|
|
116
|
+
/>
|
|
117
|
+
</Source>
|
|
118
|
+
<Source href="https://surma.dev/things/js-to-asc/">
|
|
119
|
+
<SourceTrigger label={2} showFavicon />
|
|
120
|
+
<SourceContent
|
|
121
|
+
title="JavaScript to AssemblyScript - Surma.dev"
|
|
122
|
+
description="Detailed performance comparison of JS vs AssemblyScript/Wasm for various workloads including DOM-heavy tasks."
|
|
123
|
+
/>
|
|
124
|
+
</Source>
|
|
125
|
+
<Source href="https://v8.dev/blog/wasm-compilation-pipeline">
|
|
126
|
+
<SourceTrigger label={3} showFavicon />
|
|
127
|
+
<SourceContent
|
|
128
|
+
title="Liftoff: V8's Wasm baseline compiler"
|
|
129
|
+
description="V8 team's analysis of Wasm compilation latency and their streaming compilation optimization."
|
|
130
|
+
/>
|
|
131
|
+
</Source>
|
|
132
|
+
<Source href="https://chromestatus.com/feature/6062715726462976">
|
|
133
|
+
<SourceTrigger label={4} showFavicon />
|
|
134
|
+
<SourceContent
|
|
135
|
+
title="WebAssembly Garbage Collection - Chrome Platform Status"
|
|
136
|
+
description="Status of the WasmGC proposal and its impact on languages targeting WebAssembly."
|
|
137
|
+
/>
|
|
138
|
+
</Source>
|
|
139
|
+
</SourceList>
|
|
140
|
+
|
|
141
|
+
<MessageActions>
|
|
142
|
+
<Button variant="ghost" size="icon-sm"><Copy class="size-3.5" /></Button>
|
|
143
|
+
<Button variant="ghost" size="icon-sm"><ThumbsUp class="size-3.5" /></Button>
|
|
144
|
+
<Button variant="ghost" size="icon-sm"><ThumbsDown class="size-3.5" /></Button>
|
|
145
|
+
</MessageActions>
|
|
146
|
+
</div>
|
|
147
|
+
</Message>
|
|
148
|
+
|
|
149
|
+
<ChatContainerScrollAnchor />
|
|
150
|
+
</ChatContainerContent>
|
|
151
|
+
</ChatContainer>
|
|
152
|
+
|
|
153
|
+
<div class="px-4 pb-4">
|
|
154
|
+
<PromptInput>
|
|
155
|
+
<PromptInputTextarea placeholder="Ask about WebAssembly..." />
|
|
156
|
+
<PromptInputActions class="justify-end">
|
|
157
|
+
<Button variant="default" size="icon-sm" class="rounded-full">
|
|
158
|
+
<ArrowUp class="size-4" />
|
|
159
|
+
</Button>
|
|
160
|
+
</PromptInputActions>
|
|
161
|
+
</PromptInput>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
),
|
|
165
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as FullChat from '../full-chat.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Getting Started" />
|
|
5
|
+
|
|
6
|
+
# Getting Started
|
|
7
|
+
|
|
8
|
+
`<kitn-chat>` is **transport-agnostic**: you give it a `messages` array, it renders the conversation, and it emits a `submit` event when the user sends. You own the request and the streaming; the component owns the UI.
|
|
9
|
+
|
|
10
|
+
## A web component in ~10 lines
|
|
11
|
+
|
|
12
|
+
Set rich data as JS **properties** and listen for **events** on the element:
|
|
13
|
+
|
|
14
|
+
```html
|
|
15
|
+
<kitn-chat id="chat" style="display:block; height:100vh;"></kitn-chat>
|
|
16
|
+
|
|
17
|
+
<script type="module">
|
|
18
|
+
import '@kitnai/chat/elements';
|
|
19
|
+
|
|
20
|
+
const chat = document.getElementById('chat');
|
|
21
|
+
chat.messages = [
|
|
22
|
+
{ id: '1', role: 'assistant', content: 'Hello! How can I help?' },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
chat.addEventListener('submit', (e) => {
|
|
26
|
+
const text = e.detail.value;
|
|
27
|
+
chat.messages = [
|
|
28
|
+
...chat.messages,
|
|
29
|
+
{ id: crypto.randomUUID(), role: 'user', content: text },
|
|
30
|
+
];
|
|
31
|
+
// ...call your model, then append an assistant message
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> Reactivity tip: assign a **new array** (and a new object for any message you change) when updating `chat.messages` — that's what triggers a re-render.
|
|
37
|
+
|
|
38
|
+
## The same thing in SolidJS
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import {
|
|
42
|
+
ChatConfig, ChatContainer, ChatContainerContent,
|
|
43
|
+
Message, MessageContent,
|
|
44
|
+
PromptInput, PromptInputTextarea, PromptInputActions,
|
|
45
|
+
} from '@kitnai/chat';
|
|
46
|
+
import { createSignal } from 'solid-js';
|
|
47
|
+
|
|
48
|
+
function Chat() {
|
|
49
|
+
const [input, setInput] = createSignal('');
|
|
50
|
+
return (
|
|
51
|
+
<ChatConfig proseSize="sm">
|
|
52
|
+
<ChatContainer class="h-full">
|
|
53
|
+
<ChatContainerContent class="space-y-4 p-4">
|
|
54
|
+
<Message>
|
|
55
|
+
<MessageContent markdown>{`Ask me anything.`}</MessageContent>
|
|
56
|
+
</Message>
|
|
57
|
+
</ChatContainerContent>
|
|
58
|
+
</ChatContainer>
|
|
59
|
+
<PromptInput value={input()} onValueChange={setInput} onSubmit={() => setInput('')}>
|
|
60
|
+
<PromptInputTextarea placeholder="Ask anything..." />
|
|
61
|
+
<PromptInputActions>{/* your buttons */}</PromptInputActions>
|
|
62
|
+
</PromptInput>
|
|
63
|
+
</ChatConfig>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## A complete example
|
|
69
|
+
|
|
70
|
+
Everything assembled into a real app — a conversation sidebar, a message thread with markdown, reasoning blocks and a tool call, a model switcher, context-usage meter, and a rich prompt input with attachments:
|
|
71
|
+
|
|
72
|
+
<Canvas of={FullChat.Default} />
|
|
73
|
+
|
|
74
|
+
Open it full-screen under **Examples → Full Chat App**, then explore each building block on its own page in the sidebar.
|
|
75
|
+
|
|
76
|
+
Ready to make it yours? See **[Theming](?path=/docs/theming--docs)**. Wiring a real model? See **[Integrations](?path=/docs/integrations--docs)**.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Installation" />
|
|
4
|
+
|
|
5
|
+
# Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @kitnai/chat
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The package ships two entry points: the SolidJS components (`@kitnai/chat`) and the framework-agnostic web components (`@kitnai/chat/elements`). Pick whichever fits your stack — you can mix them.
|
|
12
|
+
|
|
13
|
+
## SolidJS projects
|
|
14
|
+
|
|
15
|
+
SolidJS is a peer dependency:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install solid-js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Import the components and the design tokens once:
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { ChatContainer, Message, MessageContent, PromptInput } from '@kitnai/chat';
|
|
25
|
+
import '@kitnai/chat/theme.css';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The `@kitnai/chat` entry is shipped as source, so your bundler tree-shakes it down to exactly what you import.
|
|
29
|
+
|
|
30
|
+
## Web components (React, Vue, plain HTML, …)
|
|
31
|
+
|
|
32
|
+
Build the element bundle, then import it as a **side effect** — that registers `<kitn-chat>`, `<kitn-conversation-list>`, and `<kitn-prompt-input>`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm run build # emits dist/kitn-chat.es.js
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<script type="module">
|
|
40
|
+
import '@kitnai/chat/elements';
|
|
41
|
+
</script>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- The bundle is **ESM-only** and loads via `<script type="module">` in every modern browser.
|
|
45
|
+
- **SolidJS is bundled in** — the host page needs nothing else.
|
|
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
|
+
|
|
48
|
+
Head to **[Getting Started](?path=/docs/getting-started--docs)** to render your first chat.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Integrations" />
|
|
4
|
+
|
|
5
|
+
# Integrations
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Streaming from OpenRouter
|
|
10
|
+
|
|
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.
|
|
12
|
+
|
|
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.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
chat.addEventListener('submit', async (e) => {
|
|
17
|
+
const text = e.detail.value.trim();
|
|
18
|
+
if (!text) return;
|
|
19
|
+
|
|
20
|
+
// 1. Show the user message; clear the input
|
|
21
|
+
const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
|
|
22
|
+
chat.messages = history;
|
|
23
|
+
chat.value = '';
|
|
24
|
+
chat.loading = true;
|
|
25
|
+
|
|
26
|
+
// 2. Add an empty assistant message to stream into
|
|
27
|
+
const assistantId = crypto.randomUUID();
|
|
28
|
+
chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];
|
|
29
|
+
|
|
30
|
+
// In production, replace this URL with your own proxy endpoint.
|
|
31
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: {
|
|
34
|
+
'Authorization': `Bearer ${OPENROUTER_API_KEY}`, // server-side in production!
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
},
|
|
37
|
+
body: JSON.stringify({
|
|
38
|
+
model: 'anthropic/claude-sonnet-4',
|
|
39
|
+
stream: true,
|
|
40
|
+
messages: history.map((m) => ({ role: m.role, content: m.content })),
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const reader = res.body.getReader();
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
let buffer = '';
|
|
47
|
+
let answer = '';
|
|
48
|
+
|
|
49
|
+
while (true) {
|
|
50
|
+
const { value, done } = await reader.read();
|
|
51
|
+
if (done) break;
|
|
52
|
+
buffer += decoder.decode(value, { stream: true });
|
|
53
|
+
|
|
54
|
+
const lines = buffer.split('\n');
|
|
55
|
+
buffer = lines.pop(); // keep the partial last line
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const s = line.trim();
|
|
58
|
+
if (!s.startsWith('data:')) continue; // skip keep-alive comments
|
|
59
|
+
const payload = s.slice(5).trim();
|
|
60
|
+
if (payload === '[DONE]') continue;
|
|
61
|
+
try {
|
|
62
|
+
const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
|
|
63
|
+
if (!delta) continue;
|
|
64
|
+
answer += delta;
|
|
65
|
+
// New object for the streaming message so the row re-renders
|
|
66
|
+
chat.messages = chat.messages.map((m) =>
|
|
67
|
+
m.id === assistantId ? { ...m, content: answer } : m
|
|
68
|
+
);
|
|
69
|
+
} catch { /* ignore non-JSON keep-alive lines */ }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
chat.loading = false;
|
|
73
|
+
});
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Text-to-speech (TTS)
|
|
77
|
+
|
|
78
|
+
### Browser-native (zero dependencies)
|
|
79
|
+
|
|
80
|
+
Speak each reply once it finishes streaming — call `speak(answer)` right before `chat.loading = false`:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
function speak(text) {
|
|
84
|
+
if (!('speechSynthesis' in window)) return;
|
|
85
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
86
|
+
utter.lang = 'en-US';
|
|
87
|
+
speechSynthesis.cancel();
|
|
88
|
+
speechSynthesis.speak(utter);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Cloud TTS (OpenAI, ElevenLabs, …)
|
|
93
|
+
|
|
94
|
+
For higher-quality voices, have your backend call a TTS API and return audio, then play it (keep the provider key server-side):
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
async function speakCloud(text) {
|
|
98
|
+
const res = await fetch('/api/tts', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ text, voice: 'alloy' }),
|
|
102
|
+
});
|
|
103
|
+
const audio = new Audio(URL.createObjectURL(await res.blob()));
|
|
104
|
+
audio.play();
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Speech-to-text
|
|
109
|
+
|
|
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.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as ChatPanel from '../chat-panel-layout.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Introduction" />
|
|
5
|
+
|
|
6
|
+
# @kitnai/chat
|
|
7
|
+
|
|
8
|
+
**A SolidJS + Shadow-DOM web component kit for building AI chat interfaces.**
|
|
9
|
+
|
|
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
|
+
|
|
12
|
+
<Canvas of={ChatPanel.ChatGPTStyle} />
|
|
13
|
+
|
|
14
|
+
## Why @kitnai/chat
|
|
15
|
+
|
|
16
|
+
- **Two ways to use it** — import the SolidJS components for full compositional control, or drop in framework-agnostic **web components** (`<kitn-chat>`) that work in React, Vue, Svelte, or plain HTML.
|
|
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
|
+
- **Lightweight** — a markdown-only `<kitn-chat>` is **~61 KB gzip**, a single file. Syntax highlighting loads **on demand, per language, with no WASM** — and never loads at all if you don't render code.
|
|
19
|
+
- **~50 composable components** across three layers: headless primitives → UI primitives (built on [Kobalte](https://kobalte.dev)) → AI feature components.
|
|
20
|
+
- **Themeable** — restyle everything by overriding a handful of `--color-*` design tokens.
|
|
21
|
+
|
|
22
|
+
## Where to next
|
|
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
|
|
28
|
+
|
|
29
|
+
Or browse every component in isolation from the sidebar.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Meta, Canvas } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import * as TokenRef from '../token-reference.stories';
|
|
3
|
+
|
|
4
|
+
<Meta title="Theming" />
|
|
5
|
+
|
|
6
|
+
# Theming
|
|
7
|
+
|
|
8
|
+
The kit's appearance is driven entirely by **CSS custom properties** (`--color-*` design tokens) defined in `theme.css`. Override them to rebrand everything — buttons, bubbles, borders, the sidebar, code blocks — without touching component code.
|
|
9
|
+
|
|
10
|
+
## Overriding tokens
|
|
11
|
+
|
|
12
|
+
Because inherited CSS pierces the Shadow DOM boundary, setting tokens on `:root` re-themes both the SolidJS components and the web components:
|
|
13
|
+
|
|
14
|
+
```css
|
|
15
|
+
:root {
|
|
16
|
+
--color-background: #0f0f0f;
|
|
17
|
+
--color-foreground: #f5f5f5;
|
|
18
|
+
--color-primary: #7c3aed;
|
|
19
|
+
--color-primary-foreground: #ffffff;
|
|
20
|
+
--color-muted: #1e1e1e;
|
|
21
|
+
--color-border: #2a2a2a;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Scope the same variables to a parent element instead of `:root` to theme just one subtree. The tokens follow the **shadcn/ui convention**: they're *semantic* (`--color-primary`, not `--color-blue`) and come in **`X` / `X-foreground` pairs** — a surface color and the text color that sits on it.
|
|
26
|
+
|
|
27
|
+
## Live editor
|
|
28
|
+
|
|
29
|
+
Edit every token for **light and dark** modes and watch a real chat UI re-skin live — pick a preset, tweak swatches, then **Copy CSS** for a paste-ready `:root` + `.dark` block.
|
|
30
|
+
|
|
31
|
+
<a
|
|
32
|
+
href="?path=/story/theming-editor--editor"
|
|
33
|
+
style={{
|
|
34
|
+
display: 'inline-flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
gap: '0.45rem',
|
|
37
|
+
background: 'var(--color-primary, #18181b)',
|
|
38
|
+
color: 'var(--color-primary-foreground, #fafafa)',
|
|
39
|
+
padding: '0.55rem 1rem',
|
|
40
|
+
borderRadius: 'var(--radius-md, 0.5rem)',
|
|
41
|
+
fontWeight: 600,
|
|
42
|
+
fontSize: '0.875rem',
|
|
43
|
+
lineHeight: 1,
|
|
44
|
+
textDecoration: 'none',
|
|
45
|
+
boxShadow: '0 1px 2px rgba(0,0,0,0.08)',
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
Open the theme editor <span aria-hidden="true">→</span>
|
|
49
|
+
</a>
|
|
50
|
+
|
|
51
|
+
_(Opens full-screen — it needs more room than this docs column.)_
|
|
52
|
+
|
|
53
|
+
## Token reference
|
|
54
|
+
|
|
55
|
+
A complete, auto-generated reference of every token you can override — generated live from the loaded CSS, so it never drifts from `theme.css`:
|
|
56
|
+
|
|
57
|
+
<Canvas of={TokenRef.TokenReference} />
|
|
58
|
+
|
|
59
|
+
## Dark mode
|
|
60
|
+
|
|
61
|
+
The kit ships light and dark token sets. Toggle dark mode by adding the `dark` class to a root element (this Storybook's toolbar does exactly that):
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
document.documentElement.classList.toggle('dark');
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Appearance settings
|
|
68
|
+
|
|
69
|
+
`ChatConfig` (SolidJS) and matching properties on `<kitn-chat>` control non-color appearance:
|
|
70
|
+
|
|
71
|
+
| Setting | Values | Purpose |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `proseSize` | `xs` · `sm` · `base` · `lg` | Text/markdown sizing |
|
|
74
|
+
| `codeTheme` | any Shiki theme name | Syntax-highlight theme for code blocks |
|
|
75
|
+
| `codeHighlight` | `true` / `false` | Turn code highlighting off entirely (renders plain text, loads no Shiki) |
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
<ChatConfig proseSize="base" codeTheme="github-dark-dimmed">
|
|
79
|
+
{/* ... */}
|
|
80
|
+
</ChatConfig>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<kitn-chat prose-size="base" code-theme="github-dark-dimmed"></kitn-chat>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
That's the whole theming surface — tokens for color, `ChatConfig` for sizing and code. Everything else is encapsulated.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ChatScene } from '../../chat-scene';
|
|
2
|
+
|
|
3
|
+
/** Class the editor targets to write the active mode's token values directly onto
|
|
4
|
+
* this wrapper — so the preview reflects the editor's mode independently of any
|
|
5
|
+
* ancestor `.dark` (e.g. Storybook's own dark theme), and only the canvas reskins. */
|
|
6
|
+
export const CANVAS_CLASS = 'kitn-editor-canvas';
|
|
7
|
+
|
|
8
|
+
/** Live preview: the real chat app scene + a small rail for tokens the chat
|
|
9
|
+
* doesn't naturally surface. The editor sets the active palette on CANVAS_CLASS;
|
|
10
|
+
* the `.dark` class is also applied for any `dark:`-keyed component styling. */
|
|
11
|
+
export function Canvas(props: { mode: 'light' | 'dark' }) {
|
|
12
|
+
return (
|
|
13
|
+
<div classList={{ [CANVAS_CLASS]: true, dark: props.mode === 'dark' }} class="h-full">
|
|
14
|
+
<div class="h-full flex flex-col rounded-xl border border-border overflow-hidden bg-background">
|
|
15
|
+
{/* The real product UI — same component the Full Chat App example uses */}
|
|
16
|
+
<div class="flex-1 min-h-0">
|
|
17
|
+
<ChatScene class="h-full" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
{/* Coverage rail: tokens the chat scene doesn't surface at rest */}
|
|
21
|
+
<div class="shrink-0 border-t border-border px-4 py-2.5 flex flex-wrap items-center gap-2 bg-background text-foreground">
|
|
22
|
+
<span class="text-[11px] text-muted-foreground mr-1">Other tokens:</span>
|
|
23
|
+
<button class="bg-destructive text-destructive-foreground rounded-md px-3 h-8 text-xs font-medium">Destructive</button>
|
|
24
|
+
<button class="bg-secondary text-secondary-foreground rounded-md px-3 h-8 text-xs font-medium">Secondary</button>
|
|
25
|
+
<span class="bg-accent text-accent-foreground rounded-md px-2.5 py-1.5 text-xs">Accent</span>
|
|
26
|
+
<span class="bg-popover text-popover-foreground border border-border shadow rounded-md px-2.5 py-1.5 text-xs">Popover</span>
|
|
27
|
+
<input class="bg-input border border-border rounded-md px-2 h-8 text-xs ring-2 ring-ring" placeholder="Focus ring" />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// src/stories/docs/theme-editor/inspector.tsx
|
|
2
|
+
import { For, type JSX } from 'solid-js';
|
|
3
|
+
import { toHex, PURPOSE } from '../theme-tokens';
|
|
4
|
+
import type { Palette } from './theme-css';
|
|
5
|
+
|
|
6
|
+
const RADIUS_MAX = 1.4; // rem
|
|
7
|
+
|
|
8
|
+
export function Inspector(props: {
|
|
9
|
+
tokens: string[]; // ordered '--color-*' names
|
|
10
|
+
values: Palette; // active-mode palette (drives swatch colors)
|
|
11
|
+
radius: string; // e.g. '0.6rem'
|
|
12
|
+
onColorChange: (token: string, hex: string) => void;
|
|
13
|
+
onRadiusChange: (rem: string) => void;
|
|
14
|
+
}) {
|
|
15
|
+
const radiusRem = () => parseFloat(props.radius) || 0;
|
|
16
|
+
const swatch: JSX.CSSProperties = {
|
|
17
|
+
width: '1.75rem', height: '1.75rem', padding: '0', border: '1px solid var(--color-border)',
|
|
18
|
+
'border-radius': '6px', background: 'none', cursor: 'pointer', 'flex-shrink': '0',
|
|
19
|
+
};
|
|
20
|
+
return (
|
|
21
|
+
<div class="p-3 text-foreground">
|
|
22
|
+
<div class="text-xs font-semibold mb-2.5 text-muted-foreground uppercase tracking-wide">Tokens</div>
|
|
23
|
+
<div class="flex flex-col gap-2.5">
|
|
24
|
+
<For each={props.tokens}>
|
|
25
|
+
{(name) => {
|
|
26
|
+
const hex = () => {
|
|
27
|
+
try {
|
|
28
|
+
return toHex(props.values[name]);
|
|
29
|
+
} catch {
|
|
30
|
+
return '#888888';
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
return (
|
|
34
|
+
<label class="flex items-center gap-2.5">
|
|
35
|
+
<input
|
|
36
|
+
type="color"
|
|
37
|
+
value={hex()}
|
|
38
|
+
onInput={(e) => props.onColorChange(name, e.currentTarget.value)}
|
|
39
|
+
style={swatch}
|
|
40
|
+
/>
|
|
41
|
+
<span class="flex min-w-0 flex-col leading-tight">
|
|
42
|
+
<span class="text-sm font-medium truncate">{name.replace('--color-', '')}</span>
|
|
43
|
+
<span class="text-xs text-muted-foreground truncate">{PURPOSE[name] ?? ''}</span>
|
|
44
|
+
</span>
|
|
45
|
+
</label>
|
|
46
|
+
);
|
|
47
|
+
}}
|
|
48
|
+
</For>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="text-xs font-semibold mt-5 mb-2.5 text-muted-foreground uppercase tracking-wide">Radius</div>
|
|
52
|
+
<label class="flex items-center gap-2.5 text-sm">
|
|
53
|
+
<input
|
|
54
|
+
type="range"
|
|
55
|
+
min="0"
|
|
56
|
+
max={RADIUS_MAX}
|
|
57
|
+
step="0.05"
|
|
58
|
+
value={radiusRem()}
|
|
59
|
+
onInput={(e) => props.onRadiusChange(`${e.currentTarget.value}rem`)}
|
|
60
|
+
class="flex-1"
|
|
61
|
+
/>
|
|
62
|
+
<span class="text-sm tabular-nums w-16 text-right">{props.radius}</span>
|
|
63
|
+
</label>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildPresets, BRAND_OVERRIDES } from './presets';
|
|
3
|
+
|
|
4
|
+
const base = {
|
|
5
|
+
light: { '--color-primary': 'hsl(240 5.9% 10%)', '--color-border': 'hsl(240 5.9% 90%)', '--radius': '0.6rem' },
|
|
6
|
+
dark: { '--color-primary': 'hsl(0 0% 98%)', '--color-border': 'hsl(240 3.7% 15.9%)' },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
describe('buildPresets', () => {
|
|
10
|
+
it('orders Default first, then the brand presets', () => {
|
|
11
|
+
expect(buildPresets(base).map((p) => p.name)).toEqual(['Default', 'Violet', 'Emerald', 'Mono']);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('Default preset equals the base palette (no drift)', () => {
|
|
15
|
+
const def = buildPresets(base).find((p) => p.name === 'Default')!;
|
|
16
|
+
expect(def.light).toEqual(base.light);
|
|
17
|
+
expect(def.dark).toEqual(base.dark);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('brand presets override primary but preserve base neutrals', () => {
|
|
21
|
+
const violet = buildPresets(base).find((p) => p.name === 'Violet')!;
|
|
22
|
+
expect(violet.light['--color-primary']).toBe(BRAND_OVERRIDES.Violet.light['--color-primary']);
|
|
23
|
+
expect(violet.light['--color-border']).toBe(base.light['--color-border']);
|
|
24
|
+
expect(violet.light['--radius']).toBe(base.light['--radius']);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not mutate the base palette', () => {
|
|
28
|
+
const snapshot = JSON.stringify(base);
|
|
29
|
+
buildPresets(base);
|
|
30
|
+
expect(JSON.stringify(base)).toBe(snapshot);
|
|
31
|
+
});
|
|
32
|
+
});
|