@kitn.ai/ui 0.14.1
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 +394 -0
- package/dist/bash-InADTalH.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -0
- package/dist/css-M7EaDHN_.js +1 -0
- package/dist/custom-elements.json +4739 -0
- package/dist/engine-javascript-vq0WuIJl.js +141 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -0
- package/dist/github-light-JYsPkUQd.js +1 -0
- package/dist/host-embed.d.ts +33 -0
- package/dist/html-CPZ3oZQ7.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/javascript-C25yR2R2.js +1 -0
- package/dist/json-DxJze_jm.js +1 -0
- package/dist/kitn-chat-provider.d.ts +2 -0
- package/dist/kitn-chat-provider.es.js +240 -0
- package/dist/kitn-chat.es.js +77 -0
- package/dist/llms/llms-full.txt +1018 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/origin.d.ts +5 -0
- package/dist/provider-runtime.d.ts +27 -0
- package/dist/schemas/card-envelope.schema.json +14 -0
- package/dist/schemas/card-event.schema.json +12 -0
- package/dist/schemas/choice.schema.json +70 -0
- package/dist/schemas/confirm.schema.json +65 -0
- package/dist/schemas/embed.schema.json +65 -0
- package/dist/schemas/form.result.schema.json +7 -0
- package/dist/schemas/form.schema.json +33 -0
- package/dist/schemas/link.schema.json +56 -0
- package/dist/schemas/tasks.result.schema.json +16 -0
- package/dist/schemas/tasks.schema.json +78 -0
- package/dist/svelte--5p79yCD.js +1 -0
- package/dist/theme.tokens.css +176 -0
- package/dist/tsx-B8rCNbgL.js +1 -0
- package/dist/typescript-RycA9KXf.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/version.d.ts +4 -0
- package/dist/vue-BmIZj4XD.js +1 -0
- package/dist/wire.d.ts +53 -0
- package/frameworks/react/index.tsx +948 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +1018 -0
- package/llms.txt +104 -0
- package/package.json +141 -0
- package/src/components/artifact.stories.tsx +138 -0
- package/src/components/artifact.tsx +743 -0
- package/src/components/attachments.stories.tsx +303 -0
- package/src/components/attachments.test.tsx +64 -0
- package/src/components/attachments.tsx +399 -0
- package/src/components/card-fallback.tsx +28 -0
- package/src/components/card-renderer.tsx +52 -0
- package/src/components/card.tsx +110 -0
- package/src/components/chain-of-thought.stories.tsx +211 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +187 -0
- package/src/components/chat-container.tsx +82 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/chat-thread.test.tsx +58 -0
- package/src/components/chat-thread.tsx +249 -0
- package/src/components/checkpoint.stories.tsx +102 -0
- package/src/components/checkpoint.tsx +88 -0
- package/src/components/choice-card.tsx +569 -0
- package/src/components/code-block.stories.tsx +150 -0
- package/src/components/code-block.tsx +105 -0
- package/src/components/component-meta.json +3735 -0
- package/src/components/confirm-card.stories.tsx +74 -0
- package/src/components/confirm-card.tsx +315 -0
- package/src/components/context.stories.tsx +229 -0
- package/src/components/context.test.tsx +133 -0
- package/src/components/context.tsx +363 -0
- package/src/components/conversation-item.stories.tsx +125 -0
- package/src/components/conversation-item.tsx +16 -0
- package/src/components/conversation-list.stories.tsx +133 -0
- package/src/components/conversation-list.tsx +101 -0
- package/src/components/default-input.test.tsx +111 -0
- package/src/components/embed.tsx +196 -0
- package/src/components/empty.stories.tsx +434 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +138 -0
- package/src/components/feedback-bar.test.tsx +103 -0
- package/src/components/feedback-bar.tsx +166 -0
- package/src/components/file-tree.stories.tsx +73 -0
- package/src/components/file-tree.tsx +383 -0
- package/src/components/file-upload.stories.tsx +156 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/form-widgets.tsx +461 -0
- package/src/components/form.tsx +873 -0
- package/src/components/image.stories.tsx +89 -0
- package/src/components/image.tsx +67 -0
- package/src/components/link-preview.tsx +194 -0
- package/src/components/loader.stories.tsx +181 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +180 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-action-bar.test.tsx +128 -0
- package/src/components/message-narrow.stories.tsx +329 -0
- package/src/components/message-skills.stories.tsx +211 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +281 -0
- package/src/components/message.tsx +227 -0
- package/src/components/model-switcher.stories.tsx +123 -0
- package/src/components/model-switcher.test.tsx +65 -0
- package/src/components/model-switcher.tsx +87 -0
- package/src/components/prompt-input.stories.tsx +222 -0
- package/src/components/prompt-input.tsx +211 -0
- package/src/components/prompt-suggestion.stories.tsx +142 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +140 -0
- package/src/components/reasoning.tsx +156 -0
- package/src/components/resizable.test.tsx +226 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +100 -0
- package/src/components/scroll-button.tsx +34 -0
- package/src/components/slash-command.stories.tsx +163 -0
- package/src/components/slash-command.tsx +232 -0
- package/src/components/source-list.test.tsx +152 -0
- package/src/components/source.stories.tsx +124 -0
- package/src/components/source.tsx +129 -0
- package/src/components/tasks-card.stories.tsx +78 -0
- package/src/components/tasks-card.tsx +432 -0
- package/src/components/text-shimmer.stories.tsx +87 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +87 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +153 -0
- package/src/components/tool.tsx +184 -0
- package/src/components/use-card-resolution.ts +28 -0
- package/src/components/voice-input.stories.tsx +83 -0
- package/src/components/voice-input.tsx +107 -0
- package/src/elements/artifact.stories.tsx +506 -0
- package/src/elements/artifact.tsx +137 -0
- package/src/elements/attachments.stories.tsx +190 -0
- package/src/elements/attachments.tsx +136 -0
- package/src/elements/card.stories.tsx +113 -0
- package/src/elements/card.tsx +40 -0
- package/src/elements/cards.stories.tsx +59 -0
- package/src/elements/cards.tsx +97 -0
- package/src/elements/catalog.stories.tsx +491 -0
- package/src/elements/chain-of-thought.declarative.test.tsx +153 -0
- package/src/elements/chain-of-thought.stories.tsx +113 -0
- package/src/elements/chain-of-thought.tsx +92 -0
- package/src/elements/chat-scope-picker.stories.tsx +73 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-types.ts +42 -0
- package/src/elements/chat-workspace.stories.tsx +288 -0
- package/src/elements/chat-workspace.tsx +161 -0
- package/src/elements/chat.stories.tsx +151 -0
- package/src/elements/chat.tsx +80 -0
- package/src/elements/checkpoint.stories.tsx +72 -0
- package/src/elements/checkpoint.tsx +47 -0
- package/src/elements/choice.stories.tsx +240 -0
- package/src/elements/choice.tsx +48 -0
- package/src/elements/code-block.stories.tsx +83 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/composed-shell.stories.tsx +316 -0
- package/src/elements/confirm-card.stories.tsx +227 -0
- package/src/elements/confirm-card.tsx +51 -0
- package/src/elements/context-meter.stories.tsx +151 -0
- package/src/elements/context-meter.tsx +121 -0
- package/src/elements/conversation-list.declarative.test.tsx +137 -0
- package/src/elements/conversation-list.stories.tsx +243 -0
- package/src/elements/conversation-list.tsx +80 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +247 -0
- package/src/elements/define.tsx +171 -0
- package/src/elements/element-meta.json +3314 -0
- package/src/elements/element-types.d.ts +710 -0
- package/src/elements/embed.stories.tsx +213 -0
- package/src/elements/embed.tsx +35 -0
- package/src/elements/empty.stories.tsx +111 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.stories.tsx +114 -0
- package/src/elements/feedback-bar.tsx +61 -0
- package/src/elements/file-tree.stories.tsx +134 -0
- package/src/elements/file-tree.tsx +52 -0
- package/src/elements/file-upload.stories.tsx +82 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/form.stories.tsx +248 -0
- package/src/elements/form.tsx +43 -0
- package/src/elements/framework-usage.json +563 -0
- package/src/elements/image.stories.tsx +71 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/link-preview.stories.tsx +209 -0
- package/src/elements/link-preview.tsx +34 -0
- package/src/elements/loader.stories.tsx +88 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.stories.tsx +76 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.declarative.test.tsx +104 -0
- package/src/elements/message-skills.stories.tsx +114 -0
- package/src/elements/message-skills.tsx +85 -0
- package/src/elements/message.stories.tsx +218 -0
- package/src/elements/message.tsx +162 -0
- package/src/elements/model-switcher.declarative.test.tsx +130 -0
- package/src/elements/model-switcher.stories.tsx +198 -0
- package/src/elements/model-switcher.tsx +93 -0
- package/src/elements/popover.stories.tsx +80 -0
- package/src/elements/popover.tsx +59 -0
- package/src/elements/prompt-input-slash-command.test.tsx +128 -0
- package/src/elements/prompt-input-stoppable.test.tsx +88 -0
- package/src/elements/prompt-input-toolbar-actions.test.tsx +120 -0
- package/src/elements/prompt-input.stories.tsx +331 -0
- package/src/elements/prompt-input.tsx +185 -0
- package/src/elements/prompt-suggestions.declarative.test.tsx +164 -0
- package/src/elements/prompt-suggestions.stories.tsx +213 -0
- package/src/elements/prompt-suggestions.tsx +94 -0
- package/src/elements/reasoning.stories.tsx +77 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +56 -0
- package/src/elements/remote.stories.tsx +314 -0
- package/src/elements/remote.tsx +221 -0
- package/src/elements/resizable.d.ts +27 -0
- package/src/elements/resizable.stories.tsx +426 -0
- package/src/elements/resizable.tsx +553 -0
- package/src/elements/response-stream.stories.tsx +79 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/scroll-button.stories.tsx +202 -0
- package/src/elements/scroll-button.test.tsx +148 -0
- package/src/elements/scroll-button.tsx +126 -0
- package/src/elements/source-list.stories.tsx +143 -0
- package/src/elements/source.stories.tsx +88 -0
- package/src/elements/source.tsx +120 -0
- package/src/elements/styles.css +199 -0
- package/src/elements/switch.stories.tsx +56 -0
- package/src/elements/switch.tsx +43 -0
- package/src/elements/tasks.stories.tsx +237 -0
- package/src/elements/tasks.tsx +46 -0
- package/src/elements/text-shimmer.stories.tsx +64 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.stories.tsx +72 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.stories.tsx +89 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.stories.tsx +88 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +253 -0
- package/src/primitives/card-contract.ts +71 -0
- package/src/primitives/card-host.tsx +35 -0
- package/src/primitives/card-registry.tsx +67 -0
- package/src/primitives/card-resolution.ts +36 -0
- package/src/primitives/card-routing.ts +79 -0
- package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
- package/src/primitives/card-schemas/card-event.schema.json +12 -0
- package/src/primitives/card-schemas/choice.schema.json +70 -0
- package/src/primitives/card-schemas/confirm.schema.json +65 -0
- package/src/primitives/card-schemas/embed.schema.json +65 -0
- package/src/primitives/card-schemas/form.result.schema.json +7 -0
- package/src/primitives/card-schemas/form.schema.json +33 -0
- package/src/primitives/card-schemas/link.schema.json +56 -0
- package/src/primitives/card-schemas/tasks.result.schema.json +16 -0
- package/src/primitives/card-schemas/tasks.schema.json +78 -0
- package/src/primitives/card-validate.ts +95 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/controllable.test.ts +47 -0
- package/src/primitives/controllable.ts +21 -0
- package/src/primitives/embed-providers.ts +254 -0
- package/src/primitives/highlighter.ts +157 -0
- package/src/primitives/link-preview.ts +87 -0
- package/src/primitives/pdf-preview.ts +121 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-resize-observer.ts +12 -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/remote/host-embed.ts +345 -0
- package/src/remote/index.ts +2 -0
- package/src/remote/origin.ts +30 -0
- package/src/remote/provider-runtime.ts +259 -0
- package/src/remote/provider.ts +2 -0
- package/src/remote/validate.ts +22 -0
- package/src/remote/version.ts +12 -0
- package/src/remote/wire.ts +48 -0
- package/src/stories/chat-panel-layout.stories.tsx +145 -0
- package/src/stories/chat-scene.tsx +571 -0
- package/src/stories/checkpoint-restore.stories.tsx +256 -0
- package/src/stories/context-usage.stories.tsx +212 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +182 -0
- package/src/stories/conversation-with-sources.stories.tsx +471 -0
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ChildElements.mdx +269 -0
- package/src/stories/docs/ForAIAgents.mdx +162 -0
- package/src/stories/docs/GettingStarted.mdx +65 -0
- package/src/stories/docs/Installation.mdx +83 -0
- package/src/stories/docs/Introduction.mdx +50 -0
- package/src/stories/docs/SolidJsAdvanced.mdx +17 -0
- package/src/stories/docs/Theming.mdx +85 -0
- package/src/stories/docs/element-controls.ts +121 -0
- package/src/stories/docs/frameworks/Angular.mdx +245 -0
- package/src/stories/docs/frameworks/Html.mdx +227 -0
- package/src/stories/docs/frameworks/Overview.mdx +47 -0
- package/src/stories/docs/frameworks/React.mdx +227 -0
- package/src/stories/docs/frameworks/Solid.mdx +173 -0
- package/src/stories/docs/frameworks/Svelte.mdx +194 -0
- package/src/stories/docs/frameworks/Vue.mdx +238 -0
- package/src/stories/docs/generative-ui-overview.mdx +186 -0
- package/src/stories/docs/recipes/SpeechToText.mdx +7 -0
- package/src/stories/docs/recipes/Streaming.mdx +68 -0
- package/src/stories/docs/recipes/TextToSpeech.mdx +35 -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 +146 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/examples/ChoosingComponents.mdx +106 -0
- package/src/stories/examples/sample-data.ts +79 -0
- package/src/stories/examples/usage/checkpoint-restore.ts +156 -0
- package/src/stories/examples/usage/context-usage.ts +591 -0
- package/src/stories/examples/usage/conversation-with-reasoning.ts +224 -0
- package/src/stories/examples/usage/conversation-with-sources.ts +589 -0
- package/src/stories/examples/usage/empty-state.ts +144 -0
- package/src/stories/examples/usage/full-chat-app.ts +277 -0
- package/src/stories/examples/usage/index.ts +63 -0
- package/src/stories/examples/usage/message-actions.ts +704 -0
- package/src/stories/examples/usage/pattern-centered-conversation.ts +446 -0
- package/src/stories/examples/usage/pattern-chat-panel-layout.ts +442 -0
- package/src/stories/examples/usage/pattern-docked-widget.ts +479 -0
- package/src/stories/examples/usage/prompt-input-variants.ts +1356 -0
- package/src/stories/examples/usage/streaming-response.ts +687 -0
- package/src/stories/examples/usage/types.ts +33 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +230 -0
- 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/prompt-input-variants.stories.tsx +456 -0
- package/src/stories/streaming-response.stories.tsx +438 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/stories/typography.stories.tsx +78 -0
- package/src/types.ts +47 -0
- package/src/ui/action-icons.ts +52 -0
- package/src/ui/avatar.stories.tsx +103 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +86 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +145 -0
- package/src/ui/button.tsx +38 -0
- package/src/ui/collapsible.stories.tsx +69 -0
- package/src/ui/collapsible.tsx +125 -0
- package/src/ui/dropdown.stories.tsx +59 -0
- package/src/ui/dropdown.tsx +196 -0
- package/src/ui/hover-card.stories.tsx +77 -0
- package/src/ui/hover-card.test.tsx +30 -0
- package/src/ui/hover-card.tsx +172 -0
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +158 -0
- package/src/ui/popover.stories.tsx +81 -0
- package/src/ui/popover.test.tsx +99 -0
- package/src/ui/popover.tsx +92 -0
- package/src/ui/resizable.stories.tsx +236 -0
- package/src/ui/resizable.tsx +576 -0
- package/src/ui/scroll-area.stories.tsx +50 -0
- package/src/ui/scroll-area.tsx +15 -0
- package/src/ui/separator.stories.tsx +81 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +337 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/switch.stories.tsx +51 -0
- package/src/ui/switch.test.tsx +59 -0
- package/src/ui/switch.tsx +62 -0
- package/src/ui/textarea.stories.tsx +76 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +74 -0
- package/src/ui/tooltip.tsx +68 -0
- package/src/utils/cn.ts +24 -0
- package/theme.css +187 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Docs/Frameworks/Vue" />
|
|
4
|
+
|
|
5
|
+
# Vue
|
|
6
|
+
|
|
7
|
+
Use the `kai-*` custom elements directly in Vue templates. Pass arrays and objects via the **`.prop`
|
|
8
|
+
modifier** so Vue sets them as live DOM properties (not stringified attributes). Events use the
|
|
9
|
+
standard `@event` syntax.
|
|
10
|
+
|
|
11
|
+
There are **two ways to build with the kit**, and you can mix them:
|
|
12
|
+
|
|
13
|
+
1. **`<kai-chat>`** — the batteries-included shell: a whole chat experience in one tag. Fastest start.
|
|
14
|
+
2. **Compose the individual elements** (`<kai-conversations>`, `<kai-markdown>`, `<kai-artifact>`, …)
|
|
15
|
+
into your own layout when you want full control.
|
|
16
|
+
|
|
17
|
+
Both are shown below.
|
|
18
|
+
|
|
19
|
+
## Install & setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm i @kitn.ai/ui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Register the custom elements **once**, as a static side-effect import in `main.ts` **before**
|
|
26
|
+
`createApp().mount()`. Vue stamps the tags at mount time — if the elements aren't registered yet,
|
|
27
|
+
array/object props set by Vue are clobbered when the elements upgrade later and the UI renders blank.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
// src/main.ts
|
|
31
|
+
import '@kitn.ai/ui/elements'; // MUST come before createApp
|
|
32
|
+
import { createApp } from 'vue';
|
|
33
|
+
import App from './App.vue';
|
|
34
|
+
|
|
35
|
+
createApp(App).mount('#app');
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tell Vue's template compiler that `kai-*` tags are native custom elements, not Vue components —
|
|
39
|
+
this prevents "Unknown custom element" warnings and ensures `.prop` bindings work correctly:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// vite.config.ts
|
|
43
|
+
import { defineConfig } from 'vite';
|
|
44
|
+
import vue from '@vitejs/plugin-vue';
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
plugins: [
|
|
48
|
+
vue({
|
|
49
|
+
template: {
|
|
50
|
+
compilerOptions: {
|
|
51
|
+
isCustomElement: (tag) => tag.startsWith('kai-'),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
],
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
No CSS to import: each element is styled inside its own Shadow DOM. Only pull in
|
|
60
|
+
`@kitn.ai/ui/theme.css` if you want to override design tokens (see **Theming**).
|
|
61
|
+
|
|
62
|
+
### TypeScript / Volar augmentation
|
|
63
|
+
|
|
64
|
+
Add a reference to the kit's type declarations once so Vue's template compiler (and Volar) knows
|
|
65
|
+
the element's attributes and properties:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// env.d.ts (or vite-env.d.ts)
|
|
69
|
+
/// <reference types="@kitn.ai/ui/elements" />
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick start — the all-in-one shell
|
|
73
|
+
|
|
74
|
+
`<kai-chat>` is **transport-agnostic**: give it a `messages` array, handle the `submit` event, and
|
|
75
|
+
stream your model's reply back into state. You own the request; the element owns the UI.
|
|
76
|
+
|
|
77
|
+
```html
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
import '@kitn.ai/ui/elements';
|
|
80
|
+
import { ref } from 'vue';
|
|
81
|
+
|
|
82
|
+
type Message = { id: string; role: 'user' | 'assistant'; content: string };
|
|
83
|
+
|
|
84
|
+
const messages = ref<Message[]>([
|
|
85
|
+
{ id: '1', role: 'assistant', content: 'Hello! How can I help?' },
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
const handleSubmit = async (e: CustomEvent<{ value: string }>) => {
|
|
89
|
+
const history = [...messages.value, { id: crypto.randomUUID(), role: 'user' as const, content: e.detail.value }];
|
|
90
|
+
messages.value = history;
|
|
91
|
+
|
|
92
|
+
const aid = crypto.randomUUID();
|
|
93
|
+
messages.value = [...history, { id: aid, role: 'assistant', content: '' }];
|
|
94
|
+
|
|
95
|
+
let answer = '';
|
|
96
|
+
for await (const token of streamFromYourAPI(history)) {
|
|
97
|
+
answer += token;
|
|
98
|
+
messages.value = messages.value.map((m) => (m.id === aid ? { ...m, content: answer } : m));
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<template>
|
|
104
|
+
<!-- The elements fill their container — use flex and let the element grow with
|
|
105
|
+
flex: 1 rather than hard-coding a height. -->
|
|
106
|
+
<div style="display: flex; flex-direction: column; height: 100dvh;">
|
|
107
|
+
<kai-chat
|
|
108
|
+
:messages.prop="messages"
|
|
109
|
+
:suggestions.prop="['Summarize the chat', 'Start fresh']"
|
|
110
|
+
style="flex: 1; min-height: 0;"
|
|
111
|
+
@kai-submit="handleSubmit"
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</template>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Go further — compose the pieces
|
|
118
|
+
|
|
119
|
+
`<kai-chat>` is one option, not the only one. Every element can be composed in your own layout.
|
|
120
|
+
Here's a multi-conversation shell — a `<kai-conversations>` sidebar next to the `<kai-chat>` thread:
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<script setup lang="ts">
|
|
124
|
+
import '@kitn.ai/ui/elements';
|
|
125
|
+
import { ref } from 'vue';
|
|
126
|
+
|
|
127
|
+
const conversations = ref([
|
|
128
|
+
{
|
|
129
|
+
id: 'c1',
|
|
130
|
+
title: 'First chat',
|
|
131
|
+
scope: { type: 'document' },
|
|
132
|
+
messageCount: 3,
|
|
133
|
+
lastMessageAt: '2026-06-01T12:00:00Z',
|
|
134
|
+
updatedAt: '2026-06-01T12:00:00Z',
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
const activeId = ref('c1');
|
|
138
|
+
const messages = ref([{ id: '1', role: 'assistant', content: 'Hi!' }]);
|
|
139
|
+
|
|
140
|
+
const onSelect = (e: CustomEvent<{ id: string }>) => {
|
|
141
|
+
activeId.value = e.detail.id;
|
|
142
|
+
// load messages for the selected conversation
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const onSubmit = (e: CustomEvent<{ value: string }>) => {
|
|
146
|
+
// send message and stream reply
|
|
147
|
+
};
|
|
148
|
+
</script>
|
|
149
|
+
|
|
150
|
+
<template>
|
|
151
|
+
<!-- Lay panels out with flex: the sidebar is fixed-width, the thread takes the rest
|
|
152
|
+
with flex: 1. The elements fill whatever box you give them. -->
|
|
153
|
+
<div style="display: flex; height: 100dvh;">
|
|
154
|
+
<kai-conversations
|
|
155
|
+
:conversations.prop="conversations"
|
|
156
|
+
:active-id="activeId"
|
|
157
|
+
style="width: 300px; flex-shrink: 0;"
|
|
158
|
+
@kai-conversation-select="onSelect"
|
|
159
|
+
@kai-new-chat="startNewConversation"
|
|
160
|
+
/>
|
|
161
|
+
<kai-chat
|
|
162
|
+
:messages.prop="messages"
|
|
163
|
+
style="flex: 1; min-width: 0;"
|
|
164
|
+
@kai-submit="onSubmit"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</template>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Make the panels resizable
|
|
171
|
+
|
|
172
|
+
Want a draggable divider between the sidebar and the thread? Wrap the panels in `<kai-resizable>`
|
|
173
|
+
with one `<kai-resizable-item>` each — the handles are inserted for you (up to 3 panels). Each item
|
|
174
|
+
takes a `size` (px or `%`) plus optional `min`/`max`; listen for `@change` (`detail.sizes`) to
|
|
175
|
+
persist the layout.
|
|
176
|
+
|
|
177
|
+
```html
|
|
178
|
+
<template>
|
|
179
|
+
<div style="display: flex; flex-direction: column; height: 100dvh;">
|
|
180
|
+
<kai-resizable orientation="horizontal" style="flex: 1; min-height: 0;">
|
|
181
|
+
<kai-resizable-item size="25%" min="200px">
|
|
182
|
+
<kai-conversations
|
|
183
|
+
:conversations.prop="conversations"
|
|
184
|
+
:active-id="activeId"
|
|
185
|
+
@kai-conversation-select="onSelect"
|
|
186
|
+
/>
|
|
187
|
+
</kai-resizable-item>
|
|
188
|
+
<kai-resizable-item>
|
|
189
|
+
<kai-chat :messages.prop="messages" @kai-submit="onSubmit" />
|
|
190
|
+
</kai-resizable-item>
|
|
191
|
+
</kai-resizable>
|
|
192
|
+
</div>
|
|
193
|
+
</template>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
You can also drop **standalone display elements** anywhere in your own UI — `<kai-markdown>`,
|
|
197
|
+
`<kai-code-block>`, `<kai-artifact>`, `<kai-reasoning>`, `<kai-tool>` — to render rich AI content
|
|
198
|
+
without adopting the whole chat. Each fills its container and is controlled via props and events.
|
|
199
|
+
|
|
200
|
+
> **See it all assembled:** **[Examples → Full Chat App](?path=/story/examples-full-chat-app--default)**
|
|
201
|
+
> wires a sidebar, threaded markdown, reasoning, a tool call, a model switcher, a context meter, and
|
|
202
|
+
> a rich prompt input into one screen — a working reference to crib from.
|
|
203
|
+
|
|
204
|
+
> **Find every element:** browse the **Components** section in the sidebar. Each element's **API**
|
|
205
|
+
> tab lists its props, events, and copy-paste usage for Vue (and every other framework).
|
|
206
|
+
|
|
207
|
+
## Props & events
|
|
208
|
+
|
|
209
|
+
The rule for all elements: **rich data (arrays, objects) goes in as DOM properties; interactions
|
|
210
|
+
come out as CustomEvents.**
|
|
211
|
+
|
|
212
|
+
In Vue templates, use the **`.prop` modifier** to bind rich data as DOM properties rather than
|
|
213
|
+
stringified HTML attributes. Scalar values (strings, booleans, numbers) can go through normal
|
|
214
|
+
attribute bindings. Events use `@event` and carry their payload in `event.detail`.
|
|
215
|
+
|
|
216
|
+
| Binding type | Vue syntax | When to use |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| Rich data (array/object) | `:messages.prop="messages"` | Always for arrays and objects |
|
|
219
|
+
| Scalar attribute | `:active-id="activeId"` or `active-id="c1"` | Strings, booleans, numbers |
|
|
220
|
+
| Event | `@kai-conversation-select="onSelect"` | All CustomEvents |
|
|
221
|
+
|
|
222
|
+
Common events and their `detail` payloads:
|
|
223
|
+
|
|
224
|
+
| Element | DOM event | `detail` |
|
|
225
|
+
|---|---|---|
|
|
226
|
+
| `kai-chat` | `submit` | `{ value: string }` |
|
|
227
|
+
| `kai-conversations` | `conversationselect` | `{ id: string }` |
|
|
228
|
+
| `kai-conversations` | `newchat` | — |
|
|
229
|
+
| `kai-conversations` | `togglesidebar` | — |
|
|
230
|
+
| `kai-resizable` | `change` | `{ sizes: string[] }` |
|
|
231
|
+
|
|
232
|
+
Example event handler pattern:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const onSelect = (e: CustomEvent<{ id: string }>) => {
|
|
236
|
+
activeId.value = e.detail.id;
|
|
237
|
+
};
|
|
238
|
+
```
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
{/* src/stories/docs/generative-ui-overview.mdx */}
|
|
2
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
3
|
+
|
|
4
|
+
<Meta title="Generative UI/Overview" />
|
|
5
|
+
|
|
6
|
+
# Generative UI
|
|
7
|
+
|
|
8
|
+
Agents don't just reply with text — they ask the chat to **render typed, interactive
|
|
9
|
+
cards**: a form to fill, an action to approve, a plan to pick from, a link to preview.
|
|
10
|
+
`@kitn.ai/ui` does this with a small, typed **Card Contract** and a turnkey dispatcher.
|
|
11
|
+
|
|
12
|
+
## The loop
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
agent/server ──CardEnvelope(s)──▶ host sets <kai-cards>.cards
|
|
16
|
+
▲ │
|
|
17
|
+
│ dispatcher renders the right kai-* by `type`
|
|
18
|
+
│ │
|
|
19
|
+
└── result / next envelopes ◀─ CardPolicy ◀─ kai-card event ◀─ user interacts
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Envelopes vs. cards
|
|
23
|
+
|
|
24
|
+
Two things are easy to conflate — keeping them straight makes the rest click:
|
|
25
|
+
|
|
26
|
+
- An **envelope** is what the agent *sends*: an addressed data wrapper, not UI. Like a
|
|
27
|
+
postal envelope, it *carries* a card spec but isn't the card itself.
|
|
28
|
+
- A **card** is what the user *sees*: the rendered, interactive UI the dispatcher mounts
|
|
29
|
+
for that envelope (a `kai-form`, `kai-confirm`, … inside the shared card chrome).
|
|
30
|
+
|
|
31
|
+
The dispatcher's whole job is **envelope in → card out** — one envelope renders one card.
|
|
32
|
+
|
|
33
|
+
Everything an agent sends is a `CardEnvelope`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
interface CardEnvelope {
|
|
37
|
+
type: string; // selects the card: form | confirm | choice | tasks | link | embed
|
|
38
|
+
id: string; // correlates every event back to this card
|
|
39
|
+
data: unknown; // follows that type's JSON Schema (shipped in dist/schemas)
|
|
40
|
+
title?: string; // optional chrome heading
|
|
41
|
+
resolution?: CardResolution; // set once the user acts → renders the read-only view
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`data` follows the `type`'s **JSON Schema**, so an agent/server can validate before
|
|
46
|
+
sending; `id` correlates every event back to this card. (For **remote** cards, a transport
|
|
47
|
+
`WireFrame` wraps this *same* envelope over `postMessage` — the envelope inside is identical;
|
|
48
|
+
see Remote cards.)
|
|
49
|
+
|
|
50
|
+
## Turnkey: drop `<kai-cards>`
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<kai-cards></kai-cards>
|
|
54
|
+
<script type="module">
|
|
55
|
+
import '@kitn.ai/ui/elements';
|
|
56
|
+
const cards = document.querySelector('kai-cards');
|
|
57
|
+
cards.cards = [{ type: 'confirm', id: 'deploy', title: 'Deploy?',
|
|
58
|
+
data: { body: 'Ship it?', actions: [{ id: 'go', label: 'Deploy', default: true }] } }];
|
|
59
|
+
cards.policy = { onAction: (id, action) => console.log(id, action) };
|
|
60
|
+
</script>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`<kai-cards>` renders one card per envelope and routes every interaction through `policy`
|
|
64
|
+
(or listen for the raw bubbling `kai-card` event). In SolidJS, use `renderCard(envelope)` /
|
|
65
|
+
`<CardRenderer>` inside a `<CardProvider>`.
|
|
66
|
+
|
|
67
|
+
## Streaming & follow-ups
|
|
68
|
+
|
|
69
|
+
Swap the `cards` array to add, replace, or remove cards — e.g. replace a `confirm` with a
|
|
70
|
+
result card once the user acts. For **live streaming into a single card**, the SolidJS
|
|
71
|
+
`<CardRenderer>` re-renders reactively as its envelope's `data` changes, so an agent can
|
|
72
|
+
stream into it as work progresses. (Keyed-by-`id` in-place updates within a `<kai-cards>`
|
|
73
|
+
list are a planned refinement.)
|
|
74
|
+
|
|
75
|
+
## Resolved cards
|
|
76
|
+
|
|
77
|
+
Interactive cards (`kai-confirm`, `kai-choice`, `kai-tasks`, `kai-form`) flip to a **chromed
|
|
78
|
+
read-only** view the moment the user acts — optimistically, before any server round-trip.
|
|
79
|
+
|
|
80
|
+
To make that survive a **reload or history re-hydration**, the card reads an optional
|
|
81
|
+
`resolution` field on its `CardEnvelope`. Persist it by feeding the card's terminal event
|
|
82
|
+
back into the array with the `applyResolution` helper:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
import { applyResolution } from '@kitn.ai/ui';
|
|
86
|
+
|
|
87
|
+
// <kai-cards> is controlled — persist the resolution so it survives reload:
|
|
88
|
+
el.addEventListener('kai-card', (e) => {
|
|
89
|
+
el.cards = applyResolution(el.cards, e.detail); // resolved cards re-hydrate read-only
|
|
90
|
+
// …then save el.cards to your store/server.
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`applyResolution` is pure and safe to call on every `kai-card` event — non-terminal events
|
|
95
|
+
(`ready` / `error` / …) and unknown card ids return the same array unchanged. The terminal
|
|
96
|
+
verbs are `action` (confirm / choice) and `submit` (tasks / form).
|
|
97
|
+
|
|
98
|
+
## Remote cards
|
|
99
|
+
|
|
100
|
+
Everything above renders cards you **bundle yourself** — the dispatcher mounts a native
|
|
101
|
+
`kai-*` element for each envelope. That native `types` seam stays the easy path: register a
|
|
102
|
+
card type, ship its element, done.
|
|
103
|
+
|
|
104
|
+
**Remote cards** are the power feature for **provider-owned, cross-origin UI** — a card
|
|
105
|
+
whose code lives on a *different* origin (a partner, a plugin, your own card service) and
|
|
106
|
+
that you don't want to bundle or trust in your DOM. `<kai-remote>` renders it inside a
|
|
107
|
+
**sandboxed cross-origin `<iframe>`** and bridges it to the host over `postMessage`, carrying
|
|
108
|
+
the **exact same** `CardEnvelope` / `CardContext` / `CardEvent` shapes and routing every
|
|
109
|
+
event through the **same `CardPolicy`** as a native card. The envelope inside is identical —
|
|
110
|
+
a transport `WireFrame` just wraps it on the wire.
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<kai-remote
|
|
114
|
+
provider-origin="https://cards.provider.example"
|
|
115
|
+
src="https://cards.provider.example/card"
|
|
116
|
+
></kai-remote>
|
|
117
|
+
<script type="module">
|
|
118
|
+
import '@kitn.ai/ui/elements'; // registers <kai-remote>
|
|
119
|
+
|
|
120
|
+
const el = document.querySelector('kai-remote');
|
|
121
|
+
// The CardEnvelope is set as a JS property — it travels down the wire unchanged.
|
|
122
|
+
el.envelope = { type: 'form', id: 'signup', title: 'Join the beta',
|
|
123
|
+
data: { type: 'object', required: ['email'],
|
|
124
|
+
properties: { email: { type: 'string', title: 'Email', format: 'email' } } } };
|
|
125
|
+
|
|
126
|
+
// Every routed event is re-emitted as a bubbling kai-card CustomEvent — persist the
|
|
127
|
+
// resolution exactly like a native card:
|
|
128
|
+
el.addEventListener('kai-card', (e) => {
|
|
129
|
+
if (e.detail.kind === 'submit') save(e.detail.data);
|
|
130
|
+
});
|
|
131
|
+
// …or pass a CardPolicy directly: el.policy = { onSubmit: (id, data) => save(data) };
|
|
132
|
+
</script>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Prefer the **imperative** SDK when you manage the lifecycle yourself (theme refresh, swapping
|
|
136
|
+
cards in one warm frame):
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { mountRemoteCard } from '@kitn.ai/ui';
|
|
140
|
+
|
|
141
|
+
const handle = mountRemoteCard({
|
|
142
|
+
container: document.querySelector('#slot'),
|
|
143
|
+
providerOrigin: 'https://cards.provider.example',
|
|
144
|
+
src: 'https://cards.provider.example/card',
|
|
145
|
+
envelope,
|
|
146
|
+
context: { theme: { mode: 'dark' }, locale: 'en', authToken },
|
|
147
|
+
policy: { onSubmit: (cardId, data) => { /* persist via applyResolution, advance the chat */ } },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
handle.updateContext({ theme: { mode: 'light' } }); // re-themes the framed card live
|
|
151
|
+
handle.update(nextEnvelope); // swap cards in the same iframe
|
|
152
|
+
handle.destroy(); // teardown + remove the frame
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Inputs.** `providerOrigin` is the exact origin (`https://…`, or `http://localhost` for
|
|
156
|
+
dev) the iframe is pinned to. `src` is the provider page URL (must be on `providerOrigin`).
|
|
157
|
+
`envelope` is the `CardEnvelope` to render. `policy` is the optional `CardPolicy`. `context`
|
|
158
|
+
carries theme/locale/`a11y` and a short-lived `authToken`.
|
|
159
|
+
|
|
160
|
+
**Theme changes remount; token/locale refreshes are silent (v1).** A host **theme change
|
|
161
|
+
re-renders (remounts) the remote card**, so any in-progress remote-card state (e.g. a
|
|
162
|
+
half-filled form) **resets on a theme toggle** — renderers apply theme imperatively at mount,
|
|
163
|
+
and v1 has no in-place renderer theme hook yet. **Token / locale / `a11y` refreshes are
|
|
164
|
+
silent** (no remount — renderers read `host.context()` on demand). A future refinement is a
|
|
165
|
+
renderer context subscription for live re-theme without remount.
|
|
166
|
+
|
|
167
|
+
**Security model.** The host mints a per-instance **nonce** and pins every inbound frame on
|
|
168
|
+
**origin + source window + nonce + negotiated protocol version** — a frame failing any check
|
|
169
|
+
is dropped, so a wrong-origin or forged `postMessage` can't drive your policy. The iframe is
|
|
170
|
+
sandboxed (`allow-scripts allow-forms allow-same-origin`, **no** `allow-popups`) and gets a
|
|
171
|
+
`no-referrer` policy. Any `authToken` you pass should be **short-lived** (the provider sees
|
|
172
|
+
it). The **host page must set `Content-Security-Policy: frame-ancestors 'self'`** (or
|
|
173
|
+
`'none'`) so it can't itself be reframed; the **provider** locks who may embed it with its
|
|
174
|
+
own `frame-ancestors` listing your exact host origin (no wildcards).
|
|
175
|
+
|
|
176
|
+
**Reference runtime.** `examples/remote-provider/` is the runnable provider iframe a real
|
|
177
|
+
provider copies — it calls `createCardBridge` (from `@kitn.ai/ui/provider`, a SolidJS-free
|
|
178
|
+
bundle) and registers one renderer per card `type`. `examples/remote-host/` is the matching
|
|
179
|
+
host page. The real cross-origin behavior is verified by the standalone Playwright suite
|
|
180
|
+
(`tests/e2e/`).
|
|
181
|
+
|
|
182
|
+
## Card elements are `kai-*` components too
|
|
183
|
+
|
|
184
|
+
Each card type (`kai-confirm`, `kai-choice`, `kai-tasks`, `kai-form`, `kai-link`, `kai-embed`, `kai-remote`) is a registered `kai-*` custom element with its own prop/event API — the same model as everything else under **Components**. They live here under **Generative UI / Cards** because they only make sense inside the dispatcher/envelope system, but they follow the same component contract: attributes, JS properties, a `kai-card` event, CSS parts. Browse **Generative UI → Cards** for the per-element API references and live demos.
|
|
185
|
+
|
|
186
|
+
See **Cards** for each card type, and **SDK** for the dispatcher in action.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Docs/Recipes/Speech-to-Text" />
|
|
4
|
+
|
|
5
|
+
# Speech-to-text
|
|
6
|
+
|
|
7
|
+
The reverse direction is built in — the kit ships `<kai-voice-input>` (and a `VoiceInput` SolidJS component). Find it in the sidebar under the component stories.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Docs/Recipes/Streaming (OpenRouter)" />
|
|
4
|
+
|
|
5
|
+
# Streaming from OpenRouter
|
|
6
|
+
|
|
7
|
+
[OpenRouter](https://openrouter.ai) exposes an OpenAI-compatible streaming API (Server-Sent Events). Wire it into the `submit` event:
|
|
8
|
+
|
|
9
|
+
> **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.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
chat.addEventListener('kai-submit', async (e) => {
|
|
13
|
+
const text = e.detail.value.trim();
|
|
14
|
+
if (!text) return;
|
|
15
|
+
|
|
16
|
+
// 1. Show the user message
|
|
17
|
+
const history = [...chat.messages, { id: crypto.randomUUID(), role: 'user', content: text }];
|
|
18
|
+
chat.messages = history;
|
|
19
|
+
chat.loading = true;
|
|
20
|
+
|
|
21
|
+
// 2. Empty assistant placeholder to stream into
|
|
22
|
+
const assistantId = crypto.randomUUID();
|
|
23
|
+
chat.messages = [...history, { id: assistantId, role: 'assistant', content: '' }];
|
|
24
|
+
|
|
25
|
+
// In production, replace this with your own proxy endpoint.
|
|
26
|
+
const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Authorization': `Bearer ${OPENROUTER_API_KEY}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
model: 'anthropic/claude-sonnet-4',
|
|
34
|
+
stream: true,
|
|
35
|
+
messages: history.map((m) => ({ role: m.role, content: m.content })),
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const reader = res.body.getReader();
|
|
40
|
+
const decoder = new TextDecoder();
|
|
41
|
+
let buffer = '';
|
|
42
|
+
let answer = '';
|
|
43
|
+
|
|
44
|
+
while (true) {
|
|
45
|
+
const { value, done } = await reader.read();
|
|
46
|
+
if (done) break;
|
|
47
|
+
buffer += decoder.decode(value, { stream: true });
|
|
48
|
+
|
|
49
|
+
const lines = buffer.split('\n');
|
|
50
|
+
buffer = lines.pop();
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const s = line.trim();
|
|
53
|
+
if (!s.startsWith('data:')) continue;
|
|
54
|
+
const payload = s.slice(5).trim();
|
|
55
|
+
if (payload === '[DONE]') continue;
|
|
56
|
+
try {
|
|
57
|
+
const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
|
|
58
|
+
if (!delta) continue;
|
|
59
|
+
answer += delta;
|
|
60
|
+
chat.messages = chat.messages.map((m) =>
|
|
61
|
+
m.id === assistantId ? { ...m, content: answer } : m
|
|
62
|
+
);
|
|
63
|
+
} catch { /* ignore non-JSON keep-alive lines */ }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
chat.loading = false;
|
|
67
|
+
});
|
|
68
|
+
```
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Docs/Recipes/Text-to-Speech" />
|
|
4
|
+
|
|
5
|
+
# Text-to-speech (TTS)
|
|
6
|
+
|
|
7
|
+
## Browser-native (zero dependencies)
|
|
8
|
+
|
|
9
|
+
Speak each reply once it finishes streaming — call `speak(answer)` right before `chat.loading = false`:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
function speak(text) {
|
|
13
|
+
if (!('speechSynthesis' in window)) return;
|
|
14
|
+
const utter = new SpeechSynthesisUtterance(text);
|
|
15
|
+
utter.lang = 'en-US';
|
|
16
|
+
speechSynthesis.cancel();
|
|
17
|
+
speechSynthesis.speak(utter);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Cloud TTS (OpenAI, ElevenLabs, …)
|
|
22
|
+
|
|
23
|
+
For higher-quality voices, have your backend call a TTS API and return audio (keep the provider key server-side):
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
async function speakCloud(text) {
|
|
27
|
+
const res = await fetch('/api/tts', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
body: JSON.stringify({ text, voice: 'alloy' }),
|
|
31
|
+
});
|
|
32
|
+
const audio = new Audio(URL.createObjectURL(await res.blob()));
|
|
33
|
+
audio.play();
|
|
34
|
+
}
|
|
35
|
+
```
|
|
@@ -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
|
+
});
|