create-kai 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/README.md +311 -0
- package/dist/index.js +4433 -0
- package/dist/templates/angular/_gitignore +7 -0
- package/dist/templates/angular/angular.json +53 -0
- package/dist/templates/angular/package.json +30 -0
- package/dist/templates/angular/src/app/app.config.ts +11 -0
- package/dist/templates/angular/src/app/app.css +36 -0
- package/dist/templates/angular/src/app/app.html +51 -0
- package/dist/templates/angular/src/app/app.ts +78 -0
- package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
- package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
- package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
- package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
- package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
- package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
- package/dist/templates/angular/src/app/types.ts +2 -0
- package/dist/templates/angular/src/chat-data.ts +106 -0
- package/dist/templates/angular/src/index.html +16 -0
- package/dist/templates/angular/src/main.ts +16 -0
- package/dist/templates/angular/src/styles.css +10 -0
- package/dist/templates/angular/tsconfig.app.json +9 -0
- package/dist/templates/angular/tsconfig.json +22 -0
- package/dist/templates/nextjs/_gitignore +11 -0
- package/dist/templates/nextjs/app/chat-data.ts +125 -0
- package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
- package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
- package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
- package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
- package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
- package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
- package/dist/templates/nextjs/app/globals.css +153 -0
- package/dist/templates/nextjs/app/hooks/index.ts +1 -0
- package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
- package/dist/templates/nextjs/app/layout.tsx +50 -0
- package/dist/templates/nextjs/app/page.tsx +17 -0
- package/dist/templates/nextjs/app/theme.ts +10 -0
- package/dist/templates/nextjs/app/workspace.tsx +148 -0
- package/dist/templates/nextjs/next.config.mjs +22 -0
- package/dist/templates/nextjs/package.json +23 -0
- package/dist/templates/nextjs/postcss.config.mjs +16 -0
- package/dist/templates/nextjs/tsconfig.json +23 -0
- package/dist/templates/react/_gitignore +6 -0
- package/dist/templates/react/index.html +16 -0
- package/dist/templates/react/package.json +25 -0
- package/dist/templates/react/src/App.tsx +108 -0
- package/dist/templates/react/src/chat-data.ts +106 -0
- package/dist/templates/react/src/components/Composer.tsx +87 -0
- package/dist/templates/react/src/components/Sidebar.tsx +38 -0
- package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
- package/dist/templates/react/src/components/ThreadView.tsx +40 -0
- package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/index.ts +2 -0
- package/dist/templates/react/src/hooks/index.ts +1 -0
- package/dist/templates/react/src/hooks/useConversations.ts +37 -0
- package/dist/templates/react/src/index.css +111 -0
- package/dist/templates/react/src/main.tsx +15 -0
- package/dist/templates/react/src/vite-env.d.ts +1 -0
- package/dist/templates/react/tsconfig.app.json +28 -0
- package/dist/templates/react/tsconfig.json +7 -0
- package/dist/templates/react/tsconfig.node.json +25 -0
- package/dist/templates/react/vite.config.ts +11 -0
- package/dist/templates/solid/_gitignore +6 -0
- package/dist/templates/solid/index.html +13 -0
- package/dist/templates/solid/package.json +23 -0
- package/dist/templates/solid/src/App.tsx +151 -0
- package/dist/templates/solid/src/chat-data.ts +234 -0
- package/dist/templates/solid/src/components/Composer.tsx +90 -0
- package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
- package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
- package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
- package/dist/templates/solid/src/components/icons/index.tsx +84 -0
- package/dist/templates/solid/src/index.tsx +5 -0
- package/dist/templates/solid/src/lib/chat.ts +64 -0
- package/dist/templates/solid/src/lib/conversations.ts +54 -0
- package/dist/templates/solid/src/lib/types.ts +2 -0
- package/dist/templates/solid/src/styles.css +11 -0
- package/dist/templates/solid/tsconfig.json +18 -0
- package/dist/templates/solid/vite.config.ts +10 -0
- package/dist/templates/svelte/_gitignore +6 -0
- package/dist/templates/svelte/index.html +16 -0
- package/dist/templates/svelte/package.json +23 -0
- package/dist/templates/svelte/src/App.svelte +116 -0
- package/dist/templates/svelte/src/app.d.ts +20 -0
- package/dist/templates/svelte/src/chat-data.ts +106 -0
- package/dist/templates/svelte/src/components/Composer.svelte +102 -0
- package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
- package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
- package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
- package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
- package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
- package/dist/templates/svelte/src/index.css +111 -0
- package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
- package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
- package/dist/templates/svelte/src/lib/types.ts +2 -0
- package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
- package/dist/templates/svelte/src/main.ts +18 -0
- package/dist/templates/svelte/svelte.config.js +8 -0
- package/dist/templates/svelte/tsconfig.app.json +28 -0
- package/dist/templates/svelte/tsconfig.json +7 -0
- package/dist/templates/svelte/tsconfig.node.json +25 -0
- package/dist/templates/svelte/vite.config.ts +15 -0
- package/dist/templates/tanstack-start/_gitignore +14 -0
- package/dist/templates/tanstack-start/package.json +28 -0
- package/dist/templates/tanstack-start/serve.mjs +129 -0
- package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
- package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
- package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
- package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
- package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
- package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
- package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
- package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
- package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
- package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
- package/dist/templates/tanstack-start/src/router.tsx +16 -0
- package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
- package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
- package/dist/templates/tanstack-start/src/styles.css +153 -0
- package/dist/templates/tanstack-start/src/theme.ts +10 -0
- package/dist/templates/tanstack-start/tsconfig.json +16 -0
- package/dist/templates/tanstack-start/vite.config.ts +28 -0
- package/dist/templates/vanilla/_gitignore +6 -0
- package/dist/templates/vanilla/index.html +16 -0
- package/dist/templates/vanilla/package.json +19 -0
- package/dist/templates/vanilla/src/chat-data.ts +106 -0
- package/dist/templates/vanilla/src/index.css +111 -0
- package/dist/templates/vanilla/src/main.ts +53 -0
- package/dist/templates/vanilla/src/state.ts +119 -0
- package/dist/templates/vanilla/src/view.ts +222 -0
- package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
- package/dist/templates/vanilla/src/voice-input.ts +55 -0
- package/dist/templates/vanilla/tsconfig.json +24 -0
- package/dist/templates/vanilla/vite.config.ts +14 -0
- package/dist/templates/vue/_gitignore +6 -0
- package/dist/templates/vue/index.html +16 -0
- package/dist/templates/vue/package.json +23 -0
- package/dist/templates/vue/src/App.vue +104 -0
- package/dist/templates/vue/src/chat-data.ts +106 -0
- package/dist/templates/vue/src/components/Composer.vue +96 -0
- package/dist/templates/vue/src/components/Sidebar.vue +48 -0
- package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
- package/dist/templates/vue/src/components/ThreadView.vue +50 -0
- package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
- package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
- package/dist/templates/vue/src/composables/index.ts +3 -0
- package/dist/templates/vue/src/composables/useChat.ts +47 -0
- package/dist/templates/vue/src/composables/useConversations.ts +42 -0
- package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
- package/dist/templates/vue/src/index.css +111 -0
- package/dist/templates/vue/src/main.ts +18 -0
- package/dist/templates/vue/src/types.ts +4 -0
- package/dist/templates/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/vue/tsconfig.app.json +29 -0
- package/dist/templates/vue/tsconfig.json +7 -0
- package/dist/templates/vue/tsconfig.node.json +25 -0
- package/dist/templates/vue/vite.config.ts +24 -0
- package/package.json +63 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type { ChatMessage } from '@kitn.ai/ui';
|
|
2
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
3
|
+
import type { AppState, Theme } from './state';
|
|
4
|
+
import { SUGGESTIONS, TRIGGERS, type Conversation } from './chat-data';
|
|
5
|
+
import { createVoiceInput } from './voice-input';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The whole view: builds the hand-composed `kai-*` workspace imperatively, wires
|
|
9
|
+
* every element's CustomEvents, and returns a `render(state)` that syncs app state
|
|
10
|
+
* onto element properties. There is no framework here, so this file owns the two
|
|
11
|
+
* cross-framework gotchas directly:
|
|
12
|
+
*
|
|
13
|
+
* 1. UPGRADE RACE. main.ts `await customElements.whenDefined(...)` BEFORE calling
|
|
14
|
+
* createView, so every element created below is already upgraded and the
|
|
15
|
+
* array/object PROPERTIES we set (messages, conversations, triggers) land.
|
|
16
|
+
* 2. BOOLEAN FLAGS as PROPERTIES. `promptInput.voice = true`, not a bare `voice`
|
|
17
|
+
* attribute (the facade's flag() reads a value-less attribute as false).
|
|
18
|
+
*
|
|
19
|
+
* The composed split is <kai-resizable>/<kai-resizable-item> (sidebar | main), the
|
|
20
|
+
* sidebar is <kai-conversations>, the messages are a <kai-thread>, and the composer
|
|
21
|
+
* is a <kai-prompt-input>. Scalars (theme, placeholder, active-id) go on as
|
|
22
|
+
* attributes; arrays/objects go on as properties.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Custom-element property shapes we set from render (typed so we avoid `any`).
|
|
26
|
+
type ConversationsEl = HTMLElement & { groups?: unknown[]; conversations?: Conversation[]; collapsed?: boolean };
|
|
27
|
+
type ThreadEl = HTMLElement & { messages?: ChatMessage[] };
|
|
28
|
+
type PromptEl = HTMLElement & {
|
|
29
|
+
value?: unknown;
|
|
30
|
+
clear?: () => void;
|
|
31
|
+
loading?: boolean;
|
|
32
|
+
suggestions?: string[];
|
|
33
|
+
triggers?: unknown[];
|
|
34
|
+
voice?: boolean;
|
|
35
|
+
};
|
|
36
|
+
type ItemEl = HTMLElement & { collapsed?: boolean };
|
|
37
|
+
|
|
38
|
+
export interface ViewCallbacks {
|
|
39
|
+
onConversationSelect(id: string): void;
|
|
40
|
+
onNewChat(): void;
|
|
41
|
+
onToggleSidebar(): void;
|
|
42
|
+
onShowSidebar(): void;
|
|
43
|
+
onToggleTheme(): void;
|
|
44
|
+
onSubmit(value: string): void;
|
|
45
|
+
onSuggestion(value: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface View {
|
|
49
|
+
render(state: AppState): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Tiny hyperscript: create an element, set string attributes, append children. */
|
|
53
|
+
function el(tag: string, attrs: Record<string, string> = {}, children: (Node | string)[] = []): HTMLElement {
|
|
54
|
+
const node = document.createElement(tag);
|
|
55
|
+
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
|
|
56
|
+
node.append(...children);
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Parse an inline SVG string to a live node (moon/sun aren't in the kit's icons). */
|
|
61
|
+
function svgIcon(markup: string): SVGElement {
|
|
62
|
+
const tpl = document.createElement('template');
|
|
63
|
+
tpl.innerHTML = markup.trim();
|
|
64
|
+
return tpl.content.firstElementChild as SVGElement;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const MOON_SVG =
|
|
68
|
+
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" slot="icon" aria-hidden="true"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>';
|
|
69
|
+
const SUN_SVG =
|
|
70
|
+
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" slot="icon" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>';
|
|
71
|
+
|
|
72
|
+
// Stable references so re-setting them on every render is a no-op (only a NEW
|
|
73
|
+
// array reference re-renders a facade). Streaming re-runs render each chunk.
|
|
74
|
+
const NO_SUGGESTIONS: string[] = [];
|
|
75
|
+
|
|
76
|
+
export function createView(root: HTMLElement, callbacks: ViewCallbacks): View {
|
|
77
|
+
// ── Build the DOM (elements are already upgraded — see gotcha 1) ────────────
|
|
78
|
+
const conversations = el('kai-conversations') as ConversationsEl;
|
|
79
|
+
const aside = el('aside', { class: 'sidebar' }, [conversations]);
|
|
80
|
+
const sidebarItem = el('kai-resizable-item', { size: '280px', min: '220px', max: '420px' }, [aside]) as ItemEl;
|
|
81
|
+
|
|
82
|
+
const showSidebarBtn = el('kai-button', { variant: 'ghost', size: 'icon', icon: 'panel-left', label: 'Show sidebar' });
|
|
83
|
+
const brand = el('span', { class: 'brand' });
|
|
84
|
+
brand.textContent = '@kitn.ai/ui · composed chat';
|
|
85
|
+
const themeToggleBtn = el('kai-button', { variant: 'ghost', size: 'icon', label: 'Toggle light/dark theme' });
|
|
86
|
+
const barLeft = el('div', { class: 'bar-left' }, [showSidebarBtn, brand]);
|
|
87
|
+
const bar = el('header', { class: 'bar' }, [barLeft, themeToggleBtn]);
|
|
88
|
+
|
|
89
|
+
const thread = el('kai-thread', { class: 'thread' }) as ThreadEl;
|
|
90
|
+
|
|
91
|
+
const promptInput = el('kai-prompt-input', { placeholder: 'Message the demo…' }) as PromptEl;
|
|
92
|
+
const hint = el('p', { class: 'composer-hint' });
|
|
93
|
+
hint.innerHTML = 'Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents';
|
|
94
|
+
const composer = el('div', { class: 'composer' }, [promptInput, hint]);
|
|
95
|
+
|
|
96
|
+
const main = el('main', { class: 'main' }, [bar, thread, composer]);
|
|
97
|
+
const mainItem = el('kai-resizable-item', {}, [main]) as ItemEl;
|
|
98
|
+
|
|
99
|
+
const resizable = el('kai-resizable', { orientation: 'horizontal' }, [sidebarItem, mainItem]);
|
|
100
|
+
const app = el('div', { class: 'app' }, [resizable]);
|
|
101
|
+
root.append(app);
|
|
102
|
+
|
|
103
|
+
// Static properties (never change) — set once. `groups` is always empty (we feed
|
|
104
|
+
// a flat `conversations`); the composer's triggers + voice flag are constant.
|
|
105
|
+
conversations.groups = [];
|
|
106
|
+
promptInput.triggers = TRIGGERS;
|
|
107
|
+
promptInput.voice = true; // gotcha 2: a truthy PROPERTY, not a bare attribute
|
|
108
|
+
|
|
109
|
+
// Persistent theme-toggle glyphs, swapped by theme (avoids re-parsing per render).
|
|
110
|
+
const moonIcon = svgIcon(MOON_SVG);
|
|
111
|
+
const sunIcon = svgIcon(SUN_SVG);
|
|
112
|
+
|
|
113
|
+
// ── Composer state (kept uncontrolled so the /+@ trigger menus keep a caret) ──
|
|
114
|
+
// liveText mirrors the input text (from kai-value-change) so voice can append to
|
|
115
|
+
// it. We NEVER assign a plain string `value` (that flips the element into
|
|
116
|
+
// controlled mode and breaks the caret-anchored menus); voice seeds a ComposerDoc
|
|
117
|
+
// and clear-on-submit goes through the element's clear() method.
|
|
118
|
+
let liveText = '';
|
|
119
|
+
const voice = createVoiceInput((transcript) => {
|
|
120
|
+
const next = (liveText ? liveText + ' ' : '') + transcript;
|
|
121
|
+
liveText = next;
|
|
122
|
+
promptInput.value = [{ type: 'text', text: next }]; // ComposerDoc seed, not a string
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ── Wire events (non-bubbling kai-* CustomEvents; kai-button uses native click) ──
|
|
126
|
+
conversations.addEventListener('kai-conversation-select', (e) =>
|
|
127
|
+
callbacks.onConversationSelect((e as CustomEvent<{ id: string }>).detail.id),
|
|
128
|
+
);
|
|
129
|
+
conversations.addEventListener('kai-new-chat', () => callbacks.onNewChat());
|
|
130
|
+
conversations.addEventListener('kai-toggle-sidebar', () => callbacks.onToggleSidebar());
|
|
131
|
+
|
|
132
|
+
showSidebarBtn.addEventListener('click', () => callbacks.onShowSidebar());
|
|
133
|
+
themeToggleBtn.addEventListener('click', () => callbacks.onToggleTheme());
|
|
134
|
+
|
|
135
|
+
thread.addEventListener('kai-message-action', (e) => {
|
|
136
|
+
const detail = (e as CustomEvent<{ messageId: string; action: string }>).detail;
|
|
137
|
+
if (detail.action !== 'speak') return;
|
|
138
|
+
// `copy` (and the feedback votes) are handled inside the element; the custom
|
|
139
|
+
// `speak` action is ours — read the message text aloud via speech synthesis.
|
|
140
|
+
const m = currentMessages.find((x) => x.id === detail.messageId);
|
|
141
|
+
if (!m) return;
|
|
142
|
+
window.speechSynthesis.cancel();
|
|
143
|
+
window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
promptInput.addEventListener('kai-voice', () => {
|
|
147
|
+
if (!voice.supported) {
|
|
148
|
+
alert('Voice input needs a Chromium browser.');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
voice.start();
|
|
152
|
+
});
|
|
153
|
+
promptInput.addEventListener('kai-value-change', (e) => {
|
|
154
|
+
liveText = (e as CustomEvent<{ value: string }>).detail.value;
|
|
155
|
+
});
|
|
156
|
+
promptInput.addEventListener('kai-submit', (e) => {
|
|
157
|
+
// Reset the mirror + clear the uncontrolled input (uncontrolled-safe: clear()
|
|
158
|
+
// fires kai-value-change internally, never a string assignment), then hand the
|
|
159
|
+
// text up. main.ts owns append + streaming.
|
|
160
|
+
liveText = '';
|
|
161
|
+
promptInput.clear?.();
|
|
162
|
+
callbacks.onSubmit((e as CustomEvent<{ value: string }>).detail.value);
|
|
163
|
+
});
|
|
164
|
+
promptInput.addEventListener('kai-suggestion-click', (e) =>
|
|
165
|
+
callbacks.onSuggestion((e as CustomEvent<{ value: string }>).detail.value),
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// ── Render: sync state onto element props ───────────────────────────────────
|
|
169
|
+
let currentMessages: ChatMessage[] = [];
|
|
170
|
+
let renderedTheme: Theme | null = null;
|
|
171
|
+
|
|
172
|
+
// Bake the per-message actions onto assistant turns, memoized on the messages
|
|
173
|
+
// reference so a theme-only render doesn't churn <kai-thread>. A NEW array from a
|
|
174
|
+
// streaming chunk produces a new baked array, which re-renders the thread.
|
|
175
|
+
let lastMessages: ChatMessage[] | null = null;
|
|
176
|
+
let lastBaked: ChatMessage[] = [];
|
|
177
|
+
function bakeActions(messages: ChatMessage[]): ChatMessage[] {
|
|
178
|
+
if (messages === lastMessages) return lastBaked;
|
|
179
|
+
lastMessages = messages;
|
|
180
|
+
lastBaked = messages.map((m): ChatMessage =>
|
|
181
|
+
m.role === 'assistant'
|
|
182
|
+
? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
|
|
183
|
+
: m,
|
|
184
|
+
);
|
|
185
|
+
return lastBaked;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function render(state: AppState): void {
|
|
189
|
+
const { theme } = state;
|
|
190
|
+
app.classList.toggle('dark', theme === 'dark');
|
|
191
|
+
|
|
192
|
+
// theme on the group AND every item so slotted chrome inherits the right tokens
|
|
193
|
+
for (const node of [resizable, sidebarItem, mainItem, conversations, thread, promptInput, showSidebarBtn, themeToggleBtn]) {
|
|
194
|
+
node.setAttribute('theme', theme);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Sidebar collapse: the item's `collapsed` prop drops the panel + its divider,
|
|
198
|
+
// and the conversations rail's `collapsed` is CONTROLLED to stay in sync.
|
|
199
|
+
sidebarItem.collapsed = state.collapsed;
|
|
200
|
+
conversations.conversations = state.conversations;
|
|
201
|
+
conversations.setAttribute('active-id', state.activeId);
|
|
202
|
+
conversations.collapsed = state.collapsed;
|
|
203
|
+
|
|
204
|
+
// The reopen button only shows while collapsed (mirrors the React/Vue v-if).
|
|
205
|
+
showSidebarBtn.style.display = state.collapsed ? '' : 'none';
|
|
206
|
+
|
|
207
|
+
// Swap the toggle glyph only when the theme actually changes.
|
|
208
|
+
if (theme !== renderedTheme) {
|
|
209
|
+
themeToggleBtn.replaceChildren(theme === 'light' ? moonIcon : sunIcon);
|
|
210
|
+
renderedTheme = theme;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
currentMessages = state.messages;
|
|
214
|
+
thread.messages = bakeActions(state.messages);
|
|
215
|
+
|
|
216
|
+
promptInput.loading = state.loading;
|
|
217
|
+
// Suggestions only in the near-empty state (mirrors the React/Vue computed).
|
|
218
|
+
promptInput.suggestions = state.messages.length <= 1 ? SUGGESTIONS : NO_SUGGESTIONS;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { render };
|
|
222
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Minimal shape of a Web Speech recognition result event, enough to read the
|
|
2
|
+
* first transcript. The full SpeechRecognition API is not in TS's lib.dom yet. */
|
|
3
|
+
interface SpeechResultEvent {
|
|
4
|
+
results: ArrayLike<ArrayLike<{ transcript: string }>>;
|
|
5
|
+
}
|
|
6
|
+
interface SpeechRecognitionLike {
|
|
7
|
+
lang: string;
|
|
8
|
+
interimResults: boolean;
|
|
9
|
+
onresult: ((event: SpeechResultEvent) => void) | null;
|
|
10
|
+
start(): void;
|
|
11
|
+
}
|
|
12
|
+
type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
|
|
13
|
+
|
|
14
|
+
/** Resolve the Chromium (`webkitSpeechRecognition`) or standard constructor, if any. */
|
|
15
|
+
function getSpeechRecognition(): SpeechRecognitionCtor | undefined {
|
|
16
|
+
if (typeof window === 'undefined') return undefined;
|
|
17
|
+
const w = window as unknown as {
|
|
18
|
+
SpeechRecognition?: SpeechRecognitionCtor;
|
|
19
|
+
webkitSpeechRecognition?: SpeechRecognitionCtor;
|
|
20
|
+
};
|
|
21
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VoiceInput {
|
|
25
|
+
/** Chromium-only availability. False elsewhere so the caller can degrade. */
|
|
26
|
+
supported: boolean;
|
|
27
|
+
/** Begin one recognition pass (a no-op when unsupported). */
|
|
28
|
+
start: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Web Speech mic to one-shot transcript. Chromium-only (`webkitSpeechRecognition`);
|
|
33
|
+
* `supported` is false elsewhere so the caller can degrade gracefully. Framework
|
|
34
|
+
* ports (the React `useVoiceInput` hook, the Vue composable) share this exact
|
|
35
|
+
* logic. It hands the raw transcript to `onTranscript` and lets the caller decide
|
|
36
|
+
* what to do with it (the composer mic appends it to the live text). No composer
|
|
37
|
+
* or DOM knowledge.
|
|
38
|
+
*
|
|
39
|
+
* @param onTranscript called with the final recognized transcript.
|
|
40
|
+
*/
|
|
41
|
+
export function createVoiceInput(onTranscript: (transcript: string) => void): VoiceInput {
|
|
42
|
+
const supported = getSpeechRecognition() !== undefined;
|
|
43
|
+
|
|
44
|
+
const start = () => {
|
|
45
|
+
const Recognition = getSpeechRecognition();
|
|
46
|
+
if (!Recognition) return;
|
|
47
|
+
const rec = new Recognition();
|
|
48
|
+
rec.lang = 'en-US';
|
|
49
|
+
rec.interimResults = false;
|
|
50
|
+
rec.onresult = (event) => onTranscript(event.results[0][0].transcript);
|
|
51
|
+
rec.start();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return { supported, start };
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedSideEffectImports": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["src"]
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
|
|
3
|
+
// `@kitn.ai/ui` is linked into this example via `workspace:*`, so it resolves
|
|
4
|
+
// through node_modules + the package's `exports` map exactly like a published
|
|
5
|
+
// consumer would (no aliases, no source stubs). Build the kit first (`nx build ui`).
|
|
6
|
+
//
|
|
7
|
+
// No framework plugin here: the browser upgrades the `kai-*` custom elements
|
|
8
|
+
// natively, so unlike the React/Vue examples there is nothing for a template
|
|
9
|
+
// compiler to learn about the tags. This is the "pure web components, zero
|
|
10
|
+
// framework" showcase.
|
|
11
|
+
// https://vite.dev/config/
|
|
12
|
+
export default defineConfig({
|
|
13
|
+
server: { port: 5176 },
|
|
14
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>@kitn.ai/ui Vue example</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
html, body, #app { height: 100%; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="app"></div>
|
|
14
|
+
<script type="module" src="/src/main.ts"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitn.ai/ui-example-vue",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vue-tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "vue-tsc -b"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@kitn.ai/ui": "workspace:*",
|
|
14
|
+
"vue": "^3.5.13"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.19.21",
|
|
18
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
19
|
+
"typescript": "~5.7.2",
|
|
20
|
+
"vite": "^6.0.0",
|
|
21
|
+
"vue-tsc": "^2.2.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
4
|
+
import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from './chat-data';
|
|
5
|
+
import type { Theme } from './types';
|
|
6
|
+
import { useChat, useConversations } from './composables';
|
|
7
|
+
import Sidebar from './components/Sidebar.vue';
|
|
8
|
+
import ThreadView from './components/ThreadView.vue';
|
|
9
|
+
import Composer from './components/Composer.vue';
|
|
10
|
+
import ThemeToggle from './components/ThemeToggle.vue';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A mini chat workspace COMPOSED BY HAND from @kitn.ai/ui's individual elements —
|
|
14
|
+
* the Vue mirror of `examples/react`. It shows how the raw `kai-*` web components
|
|
15
|
+
* fit together (vs. dropping in one batteries-included `<kai-chat>`/`<kai-workspace>`):
|
|
16
|
+
*
|
|
17
|
+
* <kai-resizable>/<kai-resizable-item> — the draggable sidebar | main split (the
|
|
18
|
+
* divider is the kit's default `line` hairline)
|
|
19
|
+
* <kai-conversations> — the sidebar list (fed `conversations`, emits select/new)
|
|
20
|
+
* <kai-thread> — the scrolling message list (stick-to-bottom built in)
|
|
21
|
+
* <kai-prompt-input> — the composer at the bottom
|
|
22
|
+
*
|
|
23
|
+
* The pieces are split into `components/` (the UI subcomponents + the example's own
|
|
24
|
+
* moon/sun icons) and `composables/` (`useChat` owns the message array + streaming,
|
|
25
|
+
* `useConversations` the conversation stash, `useVoiceInput` the mic). Everything
|
|
26
|
+
* else is plain Vue refs. Swap `mockResponse(text)` for a real `fetch` to ship.
|
|
27
|
+
*/
|
|
28
|
+
const theme = ref<Theme>('dark');
|
|
29
|
+
const collapsed = ref(false);
|
|
30
|
+
const chat = useChat(THREADS[CONVERSATIONS[0].id] ?? []);
|
|
31
|
+
const { messages, loading } = chat;
|
|
32
|
+
const { conversations, activeId, selectConversation, newChat } = useConversations(chat, CONVERSATIONS);
|
|
33
|
+
|
|
34
|
+
const suggestions = computed(() => (messages.value.length <= 1 ? SUGGESTIONS : []));
|
|
35
|
+
|
|
36
|
+
async function send(raw: string) {
|
|
37
|
+
const text = raw.trim();
|
|
38
|
+
if (!text) return;
|
|
39
|
+
// The Composer already cleared its own input; here we just append the user
|
|
40
|
+
// message and stream the (mock) assistant reply.
|
|
41
|
+
chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
42
|
+
const stream = chat.streamAssistant();
|
|
43
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that go
|
|
44
|
+
// through the SAME reader a real model's response would, so this preview
|
|
45
|
+
// exercises the real path. To go live, only this one expression changes —
|
|
46
|
+
// `mockResponse(text)` becomes a POST to your route, with
|
|
47
|
+
// toOpenAIMessages(messages.value) as the body. The line below stays.
|
|
48
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
49
|
+
stream.done();
|
|
50
|
+
}
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="app" :class="{ dark: theme === 'dark' }">
|
|
55
|
+
<!-- <kai-resizable> owns the sidebar width + the divider. The handle defaults to
|
|
56
|
+
the `line` hairline (transparent at rest, tinting on hover/drag); collapsing
|
|
57
|
+
the sidebar maps to <kai-resizable-item :collapsed>. Pass `theme` to the group
|
|
58
|
+
AND every item so slotted chrome inherits the right light/dark tokens. -->
|
|
59
|
+
<kai-resizable :theme="theme" orientation="horizontal">
|
|
60
|
+
<kai-resizable-item :theme="theme" size="280px" min="220px" max="420px" :collapsed.prop="collapsed">
|
|
61
|
+
<Sidebar
|
|
62
|
+
:theme="theme"
|
|
63
|
+
:conversations="conversations"
|
|
64
|
+
:active-id="activeId"
|
|
65
|
+
:collapsed="collapsed"
|
|
66
|
+
@select="selectConversation"
|
|
67
|
+
@new-chat="newChat"
|
|
68
|
+
@toggle="collapsed = !collapsed"
|
|
69
|
+
/>
|
|
70
|
+
</kai-resizable-item>
|
|
71
|
+
|
|
72
|
+
<kai-resizable-item :theme="theme">
|
|
73
|
+
<main class="main">
|
|
74
|
+
<header class="bar">
|
|
75
|
+
<div class="bar-left">
|
|
76
|
+
<kai-button
|
|
77
|
+
v-if="collapsed"
|
|
78
|
+
:theme="theme"
|
|
79
|
+
variant="ghost"
|
|
80
|
+
size="icon"
|
|
81
|
+
icon="panel-left"
|
|
82
|
+
label="Show sidebar"
|
|
83
|
+
@click="collapsed = false"
|
|
84
|
+
></kai-button>
|
|
85
|
+
<span class="brand">@kitn.ai/ui · composed chat</span>
|
|
86
|
+
</div>
|
|
87
|
+
<ThemeToggle :theme="theme" @toggle="theme = theme === 'light' ? 'dark' : 'light'" />
|
|
88
|
+
</header>
|
|
89
|
+
|
|
90
|
+
<ThreadView :theme="theme" :messages="messages" />
|
|
91
|
+
|
|
92
|
+
<Composer
|
|
93
|
+
:theme="theme"
|
|
94
|
+
:loading="loading"
|
|
95
|
+
:suggestions="suggestions"
|
|
96
|
+
:triggers="TRIGGERS"
|
|
97
|
+
@submit="send"
|
|
98
|
+
@suggestion="send"
|
|
99
|
+
/>
|
|
100
|
+
</main>
|
|
101
|
+
</kai-resizable-item>
|
|
102
|
+
</kai-resizable>
|
|
103
|
+
</div>
|
|
104
|
+
</template>
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Self-contained sample data + the kit's shared mock responder, no backend.
|
|
2
|
+
import type { MessagePart } from '@kitn.ai/ui';
|
|
3
|
+
import { createMockResponder } from '@kitn.ai/ui/state';
|
|
4
|
+
|
|
5
|
+
export interface Conversation {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
scope: { type: 'document' | 'collection' };
|
|
9
|
+
messageCount: number;
|
|
10
|
+
lastMessageAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Msg {
|
|
14
|
+
id: string;
|
|
15
|
+
role: 'user' | 'assistant';
|
|
16
|
+
parts: MessagePart[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function newId(): string {
|
|
20
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
21
|
+
? crypto.randomUUID()
|
|
22
|
+
: 'id-' + Math.random().toString(36).slice(2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const iso = (daysAgo = 0) => new Date(Date.now() - daysAgo * 86_400_000).toISOString();
|
|
26
|
+
|
|
27
|
+
export const CONVERSATIONS: Conversation[] = [
|
|
28
|
+
{ id: 'c1', title: 'Getting started', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(0), updatedAt: iso(0) },
|
|
29
|
+
{ id: 'c2', title: 'Composing your own chat', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(0), updatedAt: iso(0) },
|
|
30
|
+
{ id: 'c3', title: 'Theming + dark mode', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(1), updatedAt: iso(1) },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const THREADS: Record<string, Msg[]> = {
|
|
34
|
+
c1: [
|
|
35
|
+
{ id: 'c1u', role: 'user', parts: [{ type: 'text', text: 'What is @kitn.ai/ui?' }] },
|
|
36
|
+
{ id: 'c1a', role: 'assistant', parts: [{ type: 'text', text: '**@kitn.ai/ui** is a set of framework-agnostic web components for AI chat UIs: message threads, streaming, markdown, tool panels, and more. This demo composes its `<kai-conversations>`, `<kai-thread>`, and `<kai-prompt-input>` elements by hand.' }] },
|
|
37
|
+
],
|
|
38
|
+
c2: [
|
|
39
|
+
{ id: 'c2u', role: 'user', parts: [{ type: 'text', text: 'How do I build my own chat instead of dropping in `<kai-chat>`?' }] },
|
|
40
|
+
{ id: 'c2a', role: 'assistant', parts: [{ type: 'text', text: "Feed your `messages` to a `<kai-thread>`, put a `<kai-prompt-input>` below, and wire `kai-submit` to append + stream. That's exactly what this example does. Read `App.vue`." }] },
|
|
41
|
+
],
|
|
42
|
+
c3: [
|
|
43
|
+
{ id: 'c3u', role: 'user', parts: [{ type: 'text', text: 'How does dark mode work?' }] },
|
|
44
|
+
{ id: 'c3a', role: 'assistant', parts: [{ type: 'text', text: "Each element takes a `theme` prop (`light` / `dark` / `auto`). Drive it from app state. Toggle it with the button top-right. The kit's `--color-*` tokens flip under a `.dark` class for your own surrounding chrome." }] },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const SUGGESTIONS = ['What is @kitn.ai/ui?', 'How do I install it?', 'Show me some markdown'];
|
|
49
|
+
|
|
50
|
+
// Rich entity triggers for the prompt input: typing `/` opens the skills menu,
|
|
51
|
+
// `@` opens the agents menu. Each selection inserts an atomic pill. Set on
|
|
52
|
+
// <kai-prompt-input> as the `triggers` JS property (a TriggerDef[]).
|
|
53
|
+
export const TRIGGERS = [
|
|
54
|
+
{
|
|
55
|
+
char: '/',
|
|
56
|
+
kind: 'skill',
|
|
57
|
+
items: [
|
|
58
|
+
{ id: 'summarize', label: 'Summarize', description: 'Summarize the thread', promptText: 'Summarize the thread.' },
|
|
59
|
+
{ id: 'translate', label: 'Translate', description: 'Translate to English', promptText: 'Translate to English.' },
|
|
60
|
+
{ id: 'rewrite', label: 'Rewrite', description: 'Rewrite for clarity', promptText: 'Rewrite this for clarity.' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
char: '@',
|
|
65
|
+
kind: 'agent',
|
|
66
|
+
items: [
|
|
67
|
+
{ id: 'researcher', label: 'Researcher', description: 'Deep web research', group: 'Agents' },
|
|
68
|
+
{ id: 'coder', label: 'Coder', description: 'Writes and edits code', group: 'Agents' },
|
|
69
|
+
{ id: 'designer', label: 'Designer', description: 'UI and visual design', group: 'Agents' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// The replies the mock cycles through, one per turn. Markdown-rich on purpose:
|
|
75
|
+
// the thread renders markdown, code blocks and blockquotes, so the canned
|
|
76
|
+
// replies exercise that instead of only proving text arrives.
|
|
77
|
+
//
|
|
78
|
+
// The first one says it is a mock in words. That is the WEAKEST of the
|
|
79
|
+
// responder's tells and the only one a real model could imitate, which is
|
|
80
|
+
// exactly why it is not the only one — the rest are on the wire, below.
|
|
81
|
+
const MOCK_REPLIES = [
|
|
82
|
+
"This reply streams token-by-token from the kit's own mock responder. No API key, no backend, no provider was contacted. Replace `mockResponse(text)` with a real model call (Anthropic, OpenAI, your own endpoint) to ship a real app.",
|
|
83
|
+
"Install it:\n\n```bash\nnpm install @kitn.ai/ui\n```\n\nThen register the elements + load the tokens:\n\n```ts\nimport '@kitn.ai/ui/elements';\nimport '@kitn.ai/ui/theme.tokens.css';\n```\n\nNow drop `<kai-thread>` and `<kai-prompt-input>` into your template.",
|
|
84
|
+
"I render **bold**, *italic*, `inline code`, lists, and code blocks:\n\n```ts\nconst stream = chat.streamAssistant();\nawait readOpenAIStream(mockResponse(text), stream);\nstream.done();\n```\n\n> Blockquotes too.",
|
|
85
|
+
"This chat is **composed by hand**: `<kai-conversations>` for the sidebar, a `<kai-thread>` for the messages, and `<kai-prompt-input>` at the bottom, all wired with plain reactive state. Swap `mockResponse(text)` for your own model call and you're shipping.",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The kit's own mock responder, shared with the `kai` MCP scaffolder and
|
|
90
|
+
* `create-kai` so there is ONE implementation of this and not seven.
|
|
91
|
+
*
|
|
92
|
+
* It yields canned SSE frames in the OpenAI chat-completions shape, which
|
|
93
|
+
* `readOpenAIStream` parses exactly as it parses a real provider's. So this
|
|
94
|
+
* no-backend preview runs the kit's REAL streaming path — the SSE reader, the
|
|
95
|
+
* part folding, all of it — rather than a hand-rolled loop that merely
|
|
96
|
+
* resembles it. Going live then changes one expression, not the handler.
|
|
97
|
+
*
|
|
98
|
+
* It also cannot be mistaken for a real turn: the stream opens with a
|
|
99
|
+
* `: kai-mock` SSE comment, every frame carries a `_kai_mock` field, `model`
|
|
100
|
+
* reports as `kai-mock` (no provider serves that, so an echoed mock frame is
|
|
101
|
+
* rejected upstream rather than quietly believed), and usage is all zeros.
|
|
102
|
+
*
|
|
103
|
+
* MODULE scope, not per-send: the responder owns the cursor into the replies
|
|
104
|
+
* above, so rebuilding it each turn would answer with the first one forever.
|
|
105
|
+
*/
|
|
106
|
+
export const mockResponse = createMockResponder({ replies: MOCK_REPLIES });
|