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.
Files changed (176) hide show
  1. package/README.md +311 -0
  2. package/dist/index.js +4433 -0
  3. package/dist/templates/angular/_gitignore +7 -0
  4. package/dist/templates/angular/angular.json +53 -0
  5. package/dist/templates/angular/package.json +30 -0
  6. package/dist/templates/angular/src/app/app.config.ts +11 -0
  7. package/dist/templates/angular/src/app/app.css +36 -0
  8. package/dist/templates/angular/src/app/app.html +51 -0
  9. package/dist/templates/angular/src/app/app.ts +78 -0
  10. package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
  11. package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
  12. package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
  13. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
  14. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
  15. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
  16. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
  17. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
  18. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
  19. package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
  20. package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
  21. package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
  22. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
  23. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
  24. package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
  25. package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
  26. package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
  27. package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
  28. package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
  29. package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
  30. package/dist/templates/angular/src/app/types.ts +2 -0
  31. package/dist/templates/angular/src/chat-data.ts +106 -0
  32. package/dist/templates/angular/src/index.html +16 -0
  33. package/dist/templates/angular/src/main.ts +16 -0
  34. package/dist/templates/angular/src/styles.css +10 -0
  35. package/dist/templates/angular/tsconfig.app.json +9 -0
  36. package/dist/templates/angular/tsconfig.json +22 -0
  37. package/dist/templates/nextjs/_gitignore +11 -0
  38. package/dist/templates/nextjs/app/chat-data.ts +125 -0
  39. package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
  40. package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
  41. package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
  42. package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
  43. package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
  44. package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
  45. package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
  46. package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
  47. package/dist/templates/nextjs/app/globals.css +153 -0
  48. package/dist/templates/nextjs/app/hooks/index.ts +1 -0
  49. package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
  50. package/dist/templates/nextjs/app/layout.tsx +50 -0
  51. package/dist/templates/nextjs/app/page.tsx +17 -0
  52. package/dist/templates/nextjs/app/theme.ts +10 -0
  53. package/dist/templates/nextjs/app/workspace.tsx +148 -0
  54. package/dist/templates/nextjs/next.config.mjs +22 -0
  55. package/dist/templates/nextjs/package.json +23 -0
  56. package/dist/templates/nextjs/postcss.config.mjs +16 -0
  57. package/dist/templates/nextjs/tsconfig.json +23 -0
  58. package/dist/templates/react/_gitignore +6 -0
  59. package/dist/templates/react/index.html +16 -0
  60. package/dist/templates/react/package.json +25 -0
  61. package/dist/templates/react/src/App.tsx +108 -0
  62. package/dist/templates/react/src/chat-data.ts +106 -0
  63. package/dist/templates/react/src/components/Composer.tsx +87 -0
  64. package/dist/templates/react/src/components/Sidebar.tsx +38 -0
  65. package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
  66. package/dist/templates/react/src/components/ThreadView.tsx +40 -0
  67. package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
  68. package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
  69. package/dist/templates/react/src/components/icons/index.ts +2 -0
  70. package/dist/templates/react/src/hooks/index.ts +1 -0
  71. package/dist/templates/react/src/hooks/useConversations.ts +37 -0
  72. package/dist/templates/react/src/index.css +111 -0
  73. package/dist/templates/react/src/main.tsx +15 -0
  74. package/dist/templates/react/src/vite-env.d.ts +1 -0
  75. package/dist/templates/react/tsconfig.app.json +28 -0
  76. package/dist/templates/react/tsconfig.json +7 -0
  77. package/dist/templates/react/tsconfig.node.json +25 -0
  78. package/dist/templates/react/vite.config.ts +11 -0
  79. package/dist/templates/solid/_gitignore +6 -0
  80. package/dist/templates/solid/index.html +13 -0
  81. package/dist/templates/solid/package.json +23 -0
  82. package/dist/templates/solid/src/App.tsx +151 -0
  83. package/dist/templates/solid/src/chat-data.ts +234 -0
  84. package/dist/templates/solid/src/components/Composer.tsx +90 -0
  85. package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
  86. package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
  87. package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
  88. package/dist/templates/solid/src/components/icons/index.tsx +84 -0
  89. package/dist/templates/solid/src/index.tsx +5 -0
  90. package/dist/templates/solid/src/lib/chat.ts +64 -0
  91. package/dist/templates/solid/src/lib/conversations.ts +54 -0
  92. package/dist/templates/solid/src/lib/types.ts +2 -0
  93. package/dist/templates/solid/src/styles.css +11 -0
  94. package/dist/templates/solid/tsconfig.json +18 -0
  95. package/dist/templates/solid/vite.config.ts +10 -0
  96. package/dist/templates/svelte/_gitignore +6 -0
  97. package/dist/templates/svelte/index.html +16 -0
  98. package/dist/templates/svelte/package.json +23 -0
  99. package/dist/templates/svelte/src/App.svelte +116 -0
  100. package/dist/templates/svelte/src/app.d.ts +20 -0
  101. package/dist/templates/svelte/src/chat-data.ts +106 -0
  102. package/dist/templates/svelte/src/components/Composer.svelte +102 -0
  103. package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
  104. package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
  105. package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
  106. package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
  107. package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
  108. package/dist/templates/svelte/src/index.css +111 -0
  109. package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
  110. package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
  111. package/dist/templates/svelte/src/lib/types.ts +2 -0
  112. package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
  113. package/dist/templates/svelte/src/main.ts +18 -0
  114. package/dist/templates/svelte/svelte.config.js +8 -0
  115. package/dist/templates/svelte/tsconfig.app.json +28 -0
  116. package/dist/templates/svelte/tsconfig.json +7 -0
  117. package/dist/templates/svelte/tsconfig.node.json +25 -0
  118. package/dist/templates/svelte/vite.config.ts +15 -0
  119. package/dist/templates/tanstack-start/_gitignore +14 -0
  120. package/dist/templates/tanstack-start/package.json +28 -0
  121. package/dist/templates/tanstack-start/serve.mjs +129 -0
  122. package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
  123. package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
  124. package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
  125. package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
  126. package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
  127. package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
  128. package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
  129. package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
  130. package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
  131. package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
  132. package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
  133. package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
  134. package/dist/templates/tanstack-start/src/router.tsx +16 -0
  135. package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
  136. package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
  137. package/dist/templates/tanstack-start/src/styles.css +153 -0
  138. package/dist/templates/tanstack-start/src/theme.ts +10 -0
  139. package/dist/templates/tanstack-start/tsconfig.json +16 -0
  140. package/dist/templates/tanstack-start/vite.config.ts +28 -0
  141. package/dist/templates/vanilla/_gitignore +6 -0
  142. package/dist/templates/vanilla/index.html +16 -0
  143. package/dist/templates/vanilla/package.json +19 -0
  144. package/dist/templates/vanilla/src/chat-data.ts +106 -0
  145. package/dist/templates/vanilla/src/index.css +111 -0
  146. package/dist/templates/vanilla/src/main.ts +53 -0
  147. package/dist/templates/vanilla/src/state.ts +119 -0
  148. package/dist/templates/vanilla/src/view.ts +222 -0
  149. package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
  150. package/dist/templates/vanilla/src/voice-input.ts +55 -0
  151. package/dist/templates/vanilla/tsconfig.json +24 -0
  152. package/dist/templates/vanilla/vite.config.ts +14 -0
  153. package/dist/templates/vue/_gitignore +6 -0
  154. package/dist/templates/vue/index.html +16 -0
  155. package/dist/templates/vue/package.json +23 -0
  156. package/dist/templates/vue/src/App.vue +104 -0
  157. package/dist/templates/vue/src/chat-data.ts +106 -0
  158. package/dist/templates/vue/src/components/Composer.vue +96 -0
  159. package/dist/templates/vue/src/components/Sidebar.vue +48 -0
  160. package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
  161. package/dist/templates/vue/src/components/ThreadView.vue +50 -0
  162. package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
  163. package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
  164. package/dist/templates/vue/src/composables/index.ts +3 -0
  165. package/dist/templates/vue/src/composables/useChat.ts +47 -0
  166. package/dist/templates/vue/src/composables/useConversations.ts +42 -0
  167. package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
  168. package/dist/templates/vue/src/index.css +111 -0
  169. package/dist/templates/vue/src/main.ts +18 -0
  170. package/dist/templates/vue/src/types.ts +4 -0
  171. package/dist/templates/vue/src/vite-env.d.ts +1 -0
  172. package/dist/templates/vue/tsconfig.app.json +29 -0
  173. package/dist/templates/vue/tsconfig.json +7 -0
  174. package/dist/templates/vue/tsconfig.node.json +25 -0
  175. package/dist/templates/vue/vite.config.ts +24 -0
  176. package/package.json +63 -0
@@ -0,0 +1,116 @@
1
+ <script lang="ts">
2
+ import type { KaiResizableItemElement } from '@kitn.ai/ui/elements';
3
+ import { readOpenAIStream } from '@kitn.ai/ui/wire';
4
+ import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from './chat-data';
5
+ import type { Theme } from './lib/types';
6
+ import { createChat } from './lib/chat.svelte';
7
+ import { createConversations } from './lib/conversations.svelte';
8
+ import Sidebar from './components/Sidebar.svelte';
9
+ import ThreadView from './components/ThreadView.svelte';
10
+ import Composer from './components/Composer.svelte';
11
+ import ThemeToggle from './components/ThemeToggle.svelte';
12
+
13
+ /**
14
+ * A mini chat workspace COMPOSED BY HAND from @kitn.ai/ui's individual elements —
15
+ * the Svelte mirror of `examples/react` + `examples/vue`. It shows how the raw
16
+ * `kai-*` web components fit together (vs. dropping in one batteries-included
17
+ * `<kai-chat>`/`<kai-workspace>`):
18
+ *
19
+ * <kai-resizable>/<kai-resizable-item> — the draggable sidebar | main split (the
20
+ * divider is the kit's default `line` hairline)
21
+ * <kai-conversations> — the sidebar list (fed `conversations`, emits select/new)
22
+ * <kai-thread> — the scrolling message list (stick-to-bottom built in)
23
+ * <kai-prompt-input> — the composer at the bottom
24
+ *
25
+ * The pieces are split into `components/` (the UI subcomponents + the example's own
26
+ * moon/sun icons) and `lib/` (`createChat` owns the message array + streaming,
27
+ * `createConversations` the conversation stash, `createVoiceInput` the mic).
28
+ * Everything else is plain Svelte runes. Swap `mockResponse(text)` for a real
29
+ * `fetch` to ship.
30
+ */
31
+ let theme: Theme = $state('dark');
32
+ let collapsed = $state(false);
33
+ const chat = createChat(THREADS[CONVERSATIONS[0].id] ?? []);
34
+ const conversations = createConversations(chat, CONVERSATIONS);
35
+
36
+ const suggestions: string[] = $derived(chat.messages.length <= 1 ? SUGGESTIONS : []);
37
+
38
+ // The sidebar <kai-resizable-item>'s `collapsed` boolean is a DOM property the
39
+ // parent <kai-resizable> reads (reflected to an attribute the group observes), so
40
+ // drive it imperatively rather than as a bare attribute.
41
+ let sidebarItem: KaiResizableItemElement;
42
+ $effect(() => { sidebarItem.collapsed = collapsed; });
43
+
44
+ async function send(raw: string) {
45
+ const text = raw.trim();
46
+ if (!text) return;
47
+ // The Composer already cleared its own input; here we just append the user
48
+ // message and stream the (mock) assistant reply.
49
+ chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
50
+ const stream = chat.streamAssistant();
51
+ // NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that go
52
+ // through the SAME reader a real model's response would, so this preview
53
+ // exercises the real path. To go live, only this one expression changes —
54
+ // `mockResponse(text)` becomes a POST to your route, with
55
+ // toOpenAIMessages(chat.messages) as the body. The line below stays.
56
+ await readOpenAIStream(mockResponse(text), stream);
57
+ stream.done();
58
+ }
59
+ </script>
60
+
61
+ <!-- <kai-resizable> owns the sidebar width + the divider. The handle defaults to the
62
+ `line` hairline (transparent at rest, tinting on hover/drag); collapsing the
63
+ sidebar maps to <kai-resizable-item collapsed>. Pass `theme` to the group AND
64
+ every item so slotted chrome inherits the right light/dark tokens. -->
65
+ <div class="app" class:dark={theme === 'dark'}>
66
+ <kai-resizable {theme} orientation="horizontal">
67
+ <kai-resizable-item bind:this={sidebarItem} {theme} size="280px" min="220px" max="420px">
68
+ <Sidebar
69
+ {theme}
70
+ conversations={conversations.conversations}
71
+ activeId={conversations.activeId}
72
+ {collapsed}
73
+ onselect={conversations.selectConversation}
74
+ onnewchat={conversations.newChat}
75
+ ontoggle={() => (collapsed = !collapsed)}
76
+ />
77
+ </kai-resizable-item>
78
+
79
+ <kai-resizable-item {theme}>
80
+ <main class="main">
81
+ <header class="bar">
82
+ <div class="bar-left">
83
+ {#if collapsed}
84
+ <!-- Comma-separated on purpose: in runes mode Svelte reads everything after the
85
+ first uncommaed code as prose, so the space-separated form suppresses only the
86
+ first rule. Both are false positives over a shadow root that already holds a
87
+ real focusable `<button>`; see the block in components/ThemeToggle.svelte. -->
88
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
89
+ <kai-button
90
+ {theme}
91
+ variant="ghost"
92
+ size="icon"
93
+ icon="panel-left"
94
+ label="Show sidebar"
95
+ onclick={() => (collapsed = false)}
96
+ ></kai-button>
97
+ {/if}
98
+ <span class="brand">@kitn.ai/ui · composed chat</span>
99
+ </div>
100
+ <ThemeToggle {theme} ontoggle={() => (theme = theme === 'light' ? 'dark' : 'light')} />
101
+ </header>
102
+
103
+ <ThreadView {theme} messages={chat.messages} />
104
+
105
+ <Composer
106
+ {theme}
107
+ loading={chat.loading}
108
+ {suggestions}
109
+ triggers={TRIGGERS}
110
+ onsubmit={send}
111
+ onsuggestion={send}
112
+ />
113
+ </main>
114
+ </kai-resizable-item>
115
+ </kai-resizable>
116
+ </div>
@@ -0,0 +1,20 @@
1
+ /// <reference types="svelte" />
2
+ /// <reference types="vite/client" />
3
+
4
+ // The raw `kai-*` elements are DOM custom elements (not Svelte components), so they
5
+ // have no generated prop types. Declare the tags this example uses so the template
6
+ // accepts their scalar attributes (`theme`, `size`, `active-id`, …), their rich
7
+ // props, and their `onkai-*` event handlers without type errors. Rich array/object
8
+ // props and boolean flags are set imperatively as DOM PROPERTIES in each component
9
+ // (see the `bind:this` + `$effect` blocks), which is why a permissive attribute map
10
+ // here is enough.
11
+ declare namespace svelteHTML {
12
+ interface IntrinsicElements {
13
+ 'kai-resizable': Record<string, unknown>;
14
+ 'kai-resizable-item': Record<string, unknown>;
15
+ 'kai-conversations': Record<string, unknown>;
16
+ 'kai-thread': Record<string, unknown>;
17
+ 'kai-prompt-input': Record<string, unknown>;
18
+ 'kai-button': Record<string, unknown>;
19
+ }
20
+ }
@@ -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.svelte`." }] },
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 });
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ import type { KaiPromptInputElement } from '@kitn.ai/ui/elements';
3
+ import type { Theme } from '../lib/types';
4
+ import { createVoiceInput } from '../lib/voiceInput';
5
+
6
+ /**
7
+ * The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
8
+ * the element ref, a live text mirror, clear-on-submit, and seeding voice.
9
+ *
10
+ * The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
11
+ * caret. We NEVER bind a plain STRING `value`: that flips the element into
12
+ * controlled mode, which re-applies the property and collapses the shadow-DOM
13
+ * selection, disabling the caret-anchored menu. So clear-on-submit goes through
14
+ * the element's `clear()` method, and voice seeds a ComposerDoc (a non-string
15
+ * value is a one-time seed that leaves the input uncontrolled). We keep the live
16
+ * text mirrored in `liveText` (from `kai-value-change`) so voice can append.
17
+ *
18
+ * Rich props (`suggestions`, `triggers`) and boolean flags (`loading`, `voice`)
19
+ * are set imperatively as DOM PROPERTIES (`bind:this` + `$effect`): `voice` in
20
+ * particular MUST be a truthy PROPERTY (`el.voice = true`), not a bare attribute,
21
+ * or the facade reads the mic control as off.
22
+ */
23
+ let {
24
+ theme,
25
+ loading,
26
+ suggestions,
27
+ triggers,
28
+ onsubmit,
29
+ onsuggestion,
30
+ }: {
31
+ theme: Theme;
32
+ loading: boolean;
33
+ suggestions: string[];
34
+ triggers: unknown[];
35
+ onsubmit: (value: string) => void;
36
+ onsuggestion: (value: string) => void;
37
+ } = $props();
38
+
39
+ // Ref to the underlying <kai-prompt-input> for the imperative pushes (rich props,
40
+ // flags, clear() on submit, ComposerDoc seed from voice); `liveText` mirrors text.
41
+ // The kit ships a typed element interface; intersect the imperative `clear()`
42
+ // method the facade exposes (methods aren't part of the generated prop interface).
43
+ let el: KaiPromptInputElement & { clear?: () => void };
44
+ let liveText = '';
45
+
46
+ $effect(() => { el.suggestions = suggestions; });
47
+ $effect(() => { el.triggers = triggers as KaiPromptInputElement['triggers']; });
48
+ $effect(() => { el.loading = loading; });
49
+ // A truthy PROPERTY, never a bare `voice` attribute. Runs once (no reactive deps).
50
+ $effect(() => { el.voice = true; });
51
+
52
+ // Voice: on a transcript, append it to the current text and seed the (uncontrolled)
53
+ // input as a ComposerDoc. A non-string `value` is a one-time seed into the
54
+ // element's internal state; a plain STRING would instead flip it into controlled
55
+ // mode and re-break the `/`+`@` triggers and submit.
56
+ const { supported: voiceSupported, start: startVoice } = createVoiceInput((transcript) => {
57
+ const next = (liveText ? liveText + ' ' : '') + transcript;
58
+ liveText = next;
59
+ el.value = [{ type: 'text', text: next }];
60
+ });
61
+
62
+ function handleVoice() {
63
+ // Mic is Chromium-only; degrade gracefully everywhere else.
64
+ if (!voiceSupported) {
65
+ alert('Voice input needs a Chromium browser.');
66
+ return;
67
+ }
68
+ startVoice();
69
+ }
70
+
71
+ function onValueChange(e: Event) {
72
+ liveText = (e as CustomEvent<{ value: string }>).detail.value;
73
+ }
74
+
75
+ function onSubmit(e: Event) {
76
+ // Clear our own uncontrolled input first — reset the mirror + call the element's
77
+ // clear() method (uncontrolled-safe; fires kai-value-change internally, never a
78
+ // string assignment) — then hand the text up. The parent owns append + streaming.
79
+ liveText = '';
80
+ el.clear?.();
81
+ onsubmit((e as CustomEvent<{ value: string }>).detail.value);
82
+ }
83
+
84
+ function onSuggestionClick(e: Event) {
85
+ onsuggestion((e as CustomEvent<{ value: string }>).detail.value);
86
+ }
87
+ </script>
88
+
89
+ <div class="composer">
90
+ <kai-prompt-input
91
+ bind:this={el}
92
+ {theme}
93
+ placeholder="Message the demo…"
94
+ onkai-voice={handleVoice}
95
+ onkai-value-change={onValueChange}
96
+ onkai-submit={onSubmit}
97
+ onkai-suggestion-click={onSuggestionClick}
98
+ ></kai-prompt-input>
99
+ <p class="composer-hint">
100
+ Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
101
+ </p>
102
+ </div>
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { KaiConversationsElement } from '@kitn.ai/ui/elements';
3
+ import type { Theme } from '../lib/types';
4
+ import type { Conversation } from '../chat-data';
5
+
6
+ /**
7
+ * The conversation rail — a thin wrapper over `<kai-conversations>`. The `.sidebar`
8
+ * div owns the shell's right border (kept OFF the element so it follows the shell's
9
+ * light/dark tokens, not the light-mode ones the element pins on itself at `:host`).
10
+ * The rail's `collapsed` is CONTROLLED by the app's collapsed state, so it stays in
11
+ * sync with the parent `<kai-resizable-item collapsed>` and re-expands on restore;
12
+ * the internal hamburger still reports the toggle intent up via
13
+ * `onkai-toggle-sidebar`.
14
+ *
15
+ * Array/object props (`groups`, `conversations`) and the boolean `collapsed` flag
16
+ * are set imperatively as DOM PROPERTIES (`bind:this` + `$effect`) so they land on
17
+ * the upgraded element rather than as attributes. `theme` / `active-id` are scalar
18
+ * attributes.
19
+ */
20
+ let {
21
+ theme,
22
+ conversations,
23
+ activeId,
24
+ collapsed,
25
+ onselect,
26
+ onnewchat,
27
+ ontoggle,
28
+ }: {
29
+ theme: Theme;
30
+ conversations: Conversation[];
31
+ activeId: string;
32
+ collapsed: boolean;
33
+ onselect: (id: string) => void;
34
+ onnewchat: () => void;
35
+ ontoggle: () => void;
36
+ } = $props();
37
+
38
+ // The kit ships a typed element interface (via `HTMLElementTagNameMap`), so the
39
+ // `bind:this` ref + its rich props are fully typed — no casts needed.
40
+ let el: KaiConversationsElement;
41
+
42
+ // `groups` never changes, so this effect has no reactive deps and runs once.
43
+ $effect(() => { el.groups = []; });
44
+ $effect(() => { el.conversations = conversations; });
45
+ $effect(() => { el.collapsed = collapsed; });
46
+
47
+ function onSelect(e: Event) {
48
+ onselect((e as CustomEvent<{ id: string }>).detail.id);
49
+ }
50
+ </script>
51
+
52
+ <aside class="sidebar">
53
+ <kai-conversations
54
+ bind:this={el}
55
+ {theme}
56
+ active-id={activeId}
57
+ onkai-conversation-select={onSelect}
58
+ onkai-new-chat={onnewchat}
59
+ onkai-toggle-sidebar={ontoggle}
60
+ ></kai-conversations>
61
+ </aside>
@@ -0,0 +1,45 @@
1
+ <script lang="ts">
2
+ import type { Theme } from '../lib/types';
3
+ import MoonIcon from './icons/MoonIcon.svelte';
4
+ import SunIcon from './icons/SunIcon.svelte';
5
+
6
+ /**
7
+ * Light/dark switch for the top bar. Shows the moon in light mode (tap -> dark)
8
+ * and the sun in dark mode. `<kai-button>` is icon-only, so it carries the
9
+ * accessible label; the glyph is decorative (`aria-hidden`) in the `icon` slot.
10
+ * `<kai-button>` fires the native bubbling `click`, so a plain `onclick` works.
11
+ */
12
+ let { theme, ontoggle }: { theme: Theme; ontoggle: () => void } = $props();
13
+ </script>
14
+
15
+ <!--
16
+ Both suppressed rules are false positives HERE, and the justification is the
17
+ shadow root Svelte's a11y pass cannot see into. It sees an unknown tag with a
18
+ click handler; the button semantics are one level down.
19
+
20
+ Measured in chromium against this starter's own build: the accessibility tree
21
+ holds a single `button` named "Toggle light/dark theme" (named from the
22
+ `aria-label` the kit puts on its inner `<button>` from the `label` prop), and
23
+ the `<kai-button>` host is ignored as "uninteresting" and contributes no node.
24
+ Tab reaches that inner button, and Enter and Space activate it natively. The
25
+ resulting click is composed, so it crosses the shadow boundary and lands on
26
+ this `onclick`. Keyboard operation is real, not asserted.
27
+
28
+ Do NOT satisfy the second warning literally by putting a role on the host.
29
+ Measured the same way, `role="button"` there turns the one node into TWO
30
+ identically named buttons, and the outer one has tabindex -1, so it is a
31
+ control screen readers announce and keyboard users cannot reach. The kit
32
+ scrubs stray host roles for this reason (`liftRoleOffHost`, kai-message).
33
+
34
+ The codes MUST be comma-separated. In runes mode Svelte reads everything after
35
+ the first uncommaed code as prose, so the space-separated form silently
36
+ suppressed only `a11y_click_events_have_key_events` and let the other through.
37
+ -->
38
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
39
+ <kai-button {theme} variant="ghost" size="icon" label="Toggle light/dark theme" onclick={ontoggle}>
40
+ {#if theme === 'light'}
41
+ <MoonIcon slot="icon" aria-hidden="true" />
42
+ {:else}
43
+ <SunIcon slot="icon" aria-hidden="true" />
44
+ {/if}
45
+ </kai-button>
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import type { ChatMessage } from '@kitn.ai/ui';
3
+ import { partsToText } from '@kitn.ai/ui/state';
4
+ import type { KaiThreadElement } from '@kitn.ai/ui/elements';
5
+ import type { Theme } from '../lib/types';
6
+
7
+ /**
8
+ * The scrolling message list. `<kai-thread>` owns the message rendering, the
9
+ * centered fixed-width column, and stick-to-bottom scroll, so this component just
10
+ * bakes the per-message actions onto the assistant turns and wires the custom
11
+ * `speak` action to the browser's speech synthesis. `copy` (and the feedback
12
+ * votes) are handled inside the element.
13
+ *
14
+ * `messages` is set on the element as a DOM PROPERTY (`bind:this` + `$effect`); a
15
+ * NEW array reference per streaming chunk is what re-renders it. The `withActions`
16
+ * `$derived` produces that fresh array whenever `messages` changes.
17
+ */
18
+ let { theme, messages }: { theme: Theme; messages: ChatMessage[] } = $props();
19
+
20
+ let el: KaiThreadElement;
21
+
22
+ // <kai-thread> reads `actions` off each message; only assistant replies get them.
23
+ const withActions: ChatMessage[] = $derived(
24
+ messages.map((m) =>
25
+ m.role === 'assistant'
26
+ ? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
27
+ : m,
28
+ ),
29
+ );
30
+
31
+ $effect(() => { el.messages = withActions; });
32
+
33
+ function onMessageAction(e: Event) {
34
+ const detail = (e as CustomEvent<{ messageId: string; action: string }>).detail;
35
+ if (detail.action === 'speak') {
36
+ const m = messages.find((x) => x.id === detail.messageId);
37
+ if (!m) return;
38
+ window.speechSynthesis.cancel();
39
+ window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
40
+ }
41
+ }
42
+ </script>
43
+
44
+ <kai-thread
45
+ class="thread"
46
+ bind:this={el}
47
+ {theme}
48
+ onkai-message-action={onMessageAction}
49
+ ></kai-thread>
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Moon glyph — shown in light mode (tap the toggle -> dark). moon/sun aren't in
4
+ * the kit's icon set, so the example owns them. The parent (`<kai-button>`) is a
5
+ * custom element, so extra props (`slot="icon"`, `aria-hidden`) pass through as a
6
+ * prop bag; spreading `...rest` onto the root <svg> lands them on the DOM node so
7
+ * it drops into <kai-button>'s `icon` slot. Mirrors the Vue icon's fallthrough.
8
+ */
9
+ let { size = 20, ...rest }: { size?: number } & Record<string, unknown> = $props();
10
+ </script>
11
+
12
+ <svg
13
+ width={size}
14
+ height={size}
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ {...rest}
22
+ >
23
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
24
+ </svg>
@@ -0,0 +1,24 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Sun glyph — shown in dark mode (tap the toggle -> light). See MoonIcon for why
4
+ * the example owns these icons and how the spread lands `slot`/`aria-hidden` on
5
+ * the root <svg>.
6
+ */
7
+ let { size = 20, ...rest }: { size?: number } & Record<string, unknown> = $props();
8
+ </script>
9
+
10
+ <svg
11
+ width={size}
12
+ height={size}
13
+ viewBox="0 0 24 24"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ {...rest}
20
+ >
21
+ <circle cx="12" cy="12" r="4" />
22
+ <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" />
23
+ <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" />
24
+ </svg>
@@ -0,0 +1,111 @@
1
+ html,
2
+ body,
3
+ #root {
4
+ height: 100%;
5
+ margin: 0;
6
+ }
7
+
8
+ body {
9
+ font-family: system-ui, -apple-system, sans-serif;
10
+ }
11
+
12
+ /* Full-height workspace shell. Colors come from the kit's --color-* tokens, which
13
+ flip to dark under the `.dark` class we toggle on `.app`. <kai-resizable> fills
14
+ this box (its `:host` is `height:100%`) and owns the sidebar | main split, the
15
+ panel widths, and the draggable divider. */
16
+ .app {
17
+ height: 100dvh;
18
+ background: var(--color-background);
19
+ color: var(--color-foreground);
20
+ }
21
+
22
+ /* The sidebar is its own surface: `--color-muted` sits a step off `--color-background`
23
+ in both themes (near-white grey in light, a shade lighter than the base in dark). That
24
+ #f4f4f5-on-white step is subtle, so a 1px right border marks the at-rest sidebar | main
25
+ division: <kai-resizable>'s `line` handle is transparent at rest and only tints on
26
+ hover/drag, so the boundary has to come from the panel itself. The border uses
27
+ `var(--color-border)`, which now tracks the shell's light/dark tokens because the parent
28
+ <ResizableItem theme={theme}> forwards the theme (a light hairline in light mode, dark in
29
+ dark). `box-sizing: border-box` keeps the 1px inside the 280px item so it doesn't shift
30
+ the content width. Collapse is handled by the parent <ResizableItem collapsed> (the panel
31
+ + its divider drop out), so the old `.sidebar.collapsed` rule is gone. */
32
+ .sidebar {
33
+ background: var(--color-muted);
34
+ border-right: 1px solid var(--color-border);
35
+ box-sizing: border-box;
36
+ height: 100%;
37
+ min-height: 0;
38
+ overflow: hidden;
39
+ }
40
+
41
+ .sidebar > * {
42
+ display: block;
43
+ width: 100%;
44
+ height: 100%;
45
+ }
46
+
47
+ .main {
48
+ display: flex;
49
+ flex-direction: column;
50
+ min-width: 0;
51
+ height: 100%;
52
+ }
53
+
54
+ .bar {
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: space-between;
58
+ padding: 0.75rem 1rem;
59
+ border-bottom: 1px solid var(--color-border);
60
+ }
61
+
62
+ .bar-left {
63
+ display: flex;
64
+ align-items: center;
65
+ gap: 0.5rem;
66
+ }
67
+
68
+ .brand {
69
+ font-size: 0.875rem;
70
+ font-weight: 600;
71
+ color: var(--color-muted-foreground);
72
+ }
73
+
74
+ /* <kai-thread> scrolls internally and centers its own fixed-width column, so the
75
+ host just needs to fill the remaining height of the main flex column. */
76
+ .thread {
77
+ flex: 1;
78
+ min-height: 0;
79
+ }
80
+
81
+ /* No top divider above the input — the composer floats against the thread. Extra
82
+ bottom padding lifts the input + hint off the viewport edge. */
83
+ .composer {
84
+ padding: 0.75rem 1rem 1.5rem;
85
+ }
86
+
87
+ /* Match the composer to the centered message column above it. */
88
+ .composer > * {
89
+ display: block;
90
+ width: 100%;
91
+ max-width: 48rem;
92
+ margin: 0 auto;
93
+ }
94
+
95
+ /* Muted, centered helper line under the input pointing out the / and @ triggers. */
96
+ .composer-hint {
97
+ margin: 0.5rem auto 0;
98
+ font-size: 0.75rem;
99
+ text-align: center;
100
+ color: var(--color-muted-foreground);
101
+ }
102
+
103
+ .composer-hint kbd {
104
+ font-family: inherit;
105
+ font-size: 0.6875rem;
106
+ padding: 0.05rem 0.3rem;
107
+ border-radius: 0.25rem;
108
+ border: 1px solid var(--color-border);
109
+ background: var(--color-muted);
110
+ color: var(--color-foreground);
111
+ }