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,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "jsx": "react-jsx",
4
+ "moduleResolution": "Bundler",
5
+ "module": "ESNext",
6
+ "target": "ES2022",
7
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
8
+ "types": ["node"],
9
+ "skipLibCheck": true,
10
+ "strict": true,
11
+ "noEmit": true,
12
+ "verbatimModuleSyntax": true,
13
+ "allowImportingTsExtensions": true
14
+ },
15
+ "include": ["src", "vite.config.ts"]
16
+ }
@@ -0,0 +1,28 @@
1
+ import { defineConfig } from 'vite';
2
+ import { tanstackStart } from '@tanstack/react-start/plugin/vite';
3
+ import viteReact from '@vitejs/plugin-react';
4
+
5
+ // https://tanstack.com/start/latest — TanStack Start is a full-stack React
6
+ // framework: every route is server-rendered, then hydrated on the client.
7
+ //
8
+ // `@kitn.ai/ui` is built with SolidJS but ships framework-agnostic custom
9
+ // elements. `@kitn.ai/ui/react` provides typed React wrappers that register the
10
+ // elements **client-only** (inside a layout effect) and assign array/object
11
+ // props as live DOM *properties*. SSR emits the bare `<kai-*>` tags; the client
12
+ // hydrates, registers and populates them — no work needed on the server.
13
+ //
14
+ // This is the standard TanStack Start config: the Start plugin first, then
15
+ // React's. The kit needs no special handling — leaving `@kitn.ai/ui` external to
16
+ // the SSR build (the Vite default) is correct, because the per-element
17
+ // registration imports only run in the browser, so the server just renders the
18
+ // bare tags.
19
+ export default defineConfig({
20
+ server: {
21
+ port: 3000,
22
+ },
23
+ plugins: [
24
+ // TanStack Start's plugin MUST come before React's.
25
+ tanstackStart(),
26
+ viteReact(),
27
+ ],
28
+ });
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ .env
4
+ .env.local
5
+ *.log
6
+ .DS_Store
@@ -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 vanilla 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,19 @@
1
+ {
2
+ "name": "@kitn.ai/ui-example-vanilla",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc"
11
+ },
12
+ "dependencies": {
13
+ "@kitn.ai/ui": "workspace:*"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "~5.7.2",
17
+ "vite": "^6.0.0"
18
+ }
19
+ }
@@ -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 `main.ts`." }] },
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 = store.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,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
+ }
@@ -0,0 +1,53 @@
1
+ import '@kitn.ai/ui/elements'; // registers the kai-* custom elements (async)
2
+ import '@kitn.ai/ui/theme.tokens.css'; // plain --color-* tokens for the shell
3
+ import './index.css';
4
+ import { readOpenAIStream } from '@kitn.ai/ui/wire';
5
+ import { createStore } from './state';
6
+ import { createView } from './view';
7
+ import { mockResponse, newId } from './chat-data';
8
+
9
+ const root = document.getElementById('app');
10
+ if (!root) throw new Error('Root element #app not found');
11
+
12
+ // The kai-* elements register ASYNCHRONOUSLY (gotcha 1: the upgrade race). Array/
13
+ // object properties set on an element BEFORE it upgrades are lost, and raw
14
+ // web-component consumers have no upgrade-race guard (unlike the React wrappers).
15
+ // So we wait until every tag we use is defined before building the view — every
16
+ // element createView() creates is then already upgraded and our property sets land.
17
+ const TAGS = ['kai-resizable', 'kai-resizable-item', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-button'];
18
+
19
+ async function boot(host: HTMLElement): Promise<void> {
20
+ await Promise.all(TAGS.map((tag) => customElements.whenDefined(tag)));
21
+
22
+ const store = createStore();
23
+
24
+ // Append the user turn, then stream the (mock) assistant reply.
25
+ async function send(raw: string): Promise<void> {
26
+ const text = raw.trim();
27
+ if (!text) return;
28
+ store.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
29
+ const stream = store.streamAssistant();
30
+ // NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that go
31
+ // through the SAME reader a real model's response would, so this preview
32
+ // exercises the real path. To go live, only this one expression changes —
33
+ // `mockResponse(text)` becomes a POST to your route, with
34
+ // toOpenAIMessages(store.state.messages) as the body. The line below stays.
35
+ await readOpenAIStream(mockResponse(text), stream);
36
+ stream.done();
37
+ }
38
+
39
+ const view = createView(host, {
40
+ onConversationSelect: (id) => store.selectConversation(id),
41
+ onNewChat: () => store.newChat(),
42
+ onToggleSidebar: () => store.toggleCollapsed(),
43
+ onShowSidebar: () => store.setCollapsed(false),
44
+ onToggleTheme: () => store.toggleTheme(),
45
+ onSubmit: (value) => void send(value),
46
+ onSuggestion: (value) => void send(value),
47
+ });
48
+
49
+ store.subscribe(() => view.render(store.state));
50
+ view.render(store.state);
51
+ }
52
+
53
+ void boot(root);
@@ -0,0 +1,119 @@
1
+ import type { ChatMessage } from '@kitn.ai/ui';
2
+ import {
3
+ appendMessage,
4
+ createAssistantStream,
5
+ onStreamSettled,
6
+ type SetMessages,
7
+ type AssistantStream,
8
+ } from '@kitn.ai/ui/state';
9
+ import { CONVERSATIONS, THREADS, newId, type Conversation } from './chat-data';
10
+
11
+ export type Theme = 'light' | 'dark';
12
+
13
+ /**
14
+ * The whole app's state in one plain object. `view.ts` reads it in `render()` and
15
+ * syncs it onto the `kai-*` element properties; the ops below mutate it and then
16
+ * `notify()` so a single subscriber (the render) re-runs. No framework, no proxies.
17
+ */
18
+ export interface AppState {
19
+ messages: ChatMessage[];
20
+ conversations: Conversation[];
21
+ activeId: string;
22
+ theme: Theme;
23
+ collapsed: boolean;
24
+ loading: boolean;
25
+ }
26
+
27
+ export interface Store {
28
+ readonly state: AppState;
29
+ /** Register a listener; it fires after every state change. */
30
+ subscribe(fn: () => void): void;
31
+ append(msg: ChatMessage): void;
32
+ /** Start streaming an assistant turn (flips `loading`, clears it on settle). */
33
+ streamAssistant(init?: Partial<ChatMessage>): AssistantStream;
34
+ selectConversation(id: string): void;
35
+ newChat(): void;
36
+ toggleTheme(): void;
37
+ toggleCollapsed(): void;
38
+ setCollapsed(value: boolean): void;
39
+ }
40
+
41
+ /**
42
+ * Owns the message array + conversation stash + UI flags, built on the kit's
43
+ * framework-neutral state core (`@kitn.ai/ui/state`) the React `useKaiChat` hook
44
+ * and the Vue `useChat` composable also use. Every message op assigns a BRAND-NEW
45
+ * array reference (`state.messages = updater(prev)`); that fresh reference is
46
+ * exactly what re-renders `<kai-thread>` when the render re-sets its `messages`
47
+ * property. Swap `mockResponse(text)` (in main.ts) for a real `fetch` to ship.
48
+ */
49
+ export function createStore(): Store {
50
+ const state: AppState = {
51
+ messages: [...(THREADS[CONVERSATIONS[0].id] ?? [])],
52
+ conversations: [...CONVERSATIONS],
53
+ activeId: CONVERSATIONS[0].id,
54
+ theme: 'dark',
55
+ collapsed: false,
56
+ loading: false,
57
+ };
58
+
59
+ const listeners = new Set<() => void>();
60
+ const notify = () => listeners.forEach((fn) => fn());
61
+
62
+ // The one contract the state core drives: a functional-updater setter (React
63
+ // setState shape) that assigns a fresh array each call, then notifies.
64
+ const set: SetMessages = (updater) => {
65
+ state.messages = updater(state.messages);
66
+ notify();
67
+ };
68
+ const setMessages = (next: ChatMessage[]) => {
69
+ state.messages = [...next];
70
+ notify();
71
+ };
72
+
73
+ return {
74
+ state,
75
+ subscribe(fn) {
76
+ listeners.add(fn);
77
+ },
78
+ append(msg) {
79
+ set((prev) => appendMessage(prev, msg));
80
+ },
81
+ streamAssistant(init) {
82
+ state.loading = true;
83
+ notify();
84
+ return onStreamSettled(createAssistantStream(set, init), () => {
85
+ state.loading = false;
86
+ notify();
87
+ });
88
+ },
89
+ selectConversation(id) {
90
+ THREADS[state.activeId] = state.messages; // stash the open thread
91
+ state.activeId = id;
92
+ setMessages(THREADS[id] ?? []); // load the picked one (notifies)
93
+ },
94
+ newChat() {
95
+ const id = newId();
96
+ THREADS[state.activeId] = state.messages;
97
+ THREADS[id] = [];
98
+ const now = new Date().toISOString();
99
+ state.conversations = [
100
+ { id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: now, updatedAt: now },
101
+ ...state.conversations,
102
+ ];
103
+ state.activeId = id;
104
+ setMessages([]); // notifies
105
+ },
106
+ toggleTheme() {
107
+ state.theme = state.theme === 'light' ? 'dark' : 'light';
108
+ notify();
109
+ },
110
+ toggleCollapsed() {
111
+ state.collapsed = !state.collapsed;
112
+ notify();
113
+ },
114
+ setCollapsed(value) {
115
+ state.collapsed = value;
116
+ notify();
117
+ },
118
+ };
119
+ }