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
+ // NO PLUGINS, ON PURPOSE — and the reason is not "our CSS is plain".
2
+ //
3
+ // This config used to say the app imports "@kitn.ai/ui/theme.css — just custom
4
+ // properties", and that was wrong in a way that built green: `theme.css` is
5
+ // Tailwind v4 SOURCE, its light tokens live in an `@theme { … }` at-rule, and with
6
+ // no Tailwind in this pipeline the browser discarded the whole block. The app now
7
+ // imports the pre-compiled `@kitn.ai/ui/theme.tokens.css` instead (see the note in
8
+ // `app/layout.tsx`), which is real CSS and needs no plugin.
9
+ //
10
+ // The empty config still has a job: without it Next walks up the tree and picks up
11
+ // the monorepo root's Tailwind PostCSS config, which is not installed here. A
12
+ // standalone app outside this repo would not need the file at all — and an app
13
+ // that DOES add Tailwind should add the plugin here and switch the import back to
14
+ // `theme.css`, which is then compiled rather than discarded.
15
+ const config = { plugins: {} };
16
+ export default config;
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["dom", "dom.iterable", "ES2022"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -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 React example</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body, #root { height: 100%; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@kitn.ai/ui-example-react",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@kitn.ai/ui": "workspace:*",
14
+ "react": "^19.0.0",
15
+ "react-dom": "^19.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.19.21",
19
+ "@types/react": "^19.0.0",
20
+ "@types/react-dom": "^19.0.0",
21
+ "@vitejs/plugin-react": "^4.3.4",
22
+ "typescript": "~5.7.2",
23
+ "vite": "^6.0.0"
24
+ }
25
+ }
@@ -0,0 +1,108 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { Button, Resizable, ResizableItem, useKaiChat } from '@kitn.ai/ui/react';
3
+ import { readOpenAIStream } from '@kitn.ai/ui/wire';
4
+ import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from './chat-data';
5
+ import { Sidebar } from './components/Sidebar';
6
+ import { ThreadView } from './components/ThreadView';
7
+ import { Composer } from './components/Composer';
8
+ import { ThemeToggle } from './components/ThemeToggle';
9
+ import { useConversations } from './hooks';
10
+
11
+ export type Theme = 'light' | 'dark';
12
+
13
+ /**
14
+ * A mini chat **workspace composed by hand** from @kitn.ai/ui's individual
15
+ * elements — to show how the pieces fit together (vs. dropping in one
16
+ * batteries-included <kai-chat>/<kai-workspace> tag):
17
+ *
18
+ * <Resizable>/<ResizableItem> — the draggable sidebar | main split (the divider
19
+ * is the kit's default `line` hairline)
20
+ * <Conversations> — the sidebar list (fed `conversations`, emits select/new)
21
+ * <Thread> — the scrolling message list (kai-thread; stick-to-bottom built in)
22
+ * <PromptInput> — the composer at the bottom
23
+ *
24
+ * The pieces are split into `components/` (the UI subcomponents + the example's own
25
+ * moon/sun icons) and `hooks/` (the
26
+ * conversation stash; voice input + the thread scroll now come from the kit), so the
27
+ * structure reads top-down. `useKaiChat` owns the
28
+ * message array + streaming; everything else is plain React state. Swap
29
+ * `mockResponse(text)` for a real `fetch` to ship a real app.
30
+ */
31
+ export default function App() {
32
+ const [theme, setTheme] = useState<Theme>('dark');
33
+ const [collapsed, setCollapsed] = useState(false);
34
+ const chat = useKaiChat({ initialMessages: THREADS[CONVERSATIONS[0].id] ?? [] });
35
+ const { conversations, activeId, selectConversation, newChat } = useConversations(chat, CONVERSATIONS);
36
+
37
+ const send = useCallback(
38
+ async (raw: string) => {
39
+ const text = raw.trim();
40
+ if (!text) return;
41
+ // The Composer already cleared its own input; here we just append the user
42
+ // message and stream the (mock) assistant reply.
43
+ chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
44
+ const stream = chat.streamAssistant();
45
+ // NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that
46
+ // go through the SAME reader a real model's response would, so this
47
+ // preview exercises the real path. To go live, only this one expression
48
+ // changes — `mockResponse(text)` becomes a POST to your route, with
49
+ // toOpenAIMessages(chat.messages) as the body. The line below stays.
50
+ await readOpenAIStream(mockResponse(text), stream);
51
+ stream.done();
52
+ },
53
+ [chat],
54
+ );
55
+
56
+ return (
57
+ <div className={`app${theme === 'dark' ? ' dark' : ''}`}>
58
+ {/* <kai-resizable> owns the sidebar width + the divider. The handle defaults
59
+ to the new `line` hairline (transparent at rest, tinting on hover/drag);
60
+ collapsing the sidebar maps to <ResizableItem collapsed>. */}
61
+ <Resizable theme={theme} orientation="horizontal">
62
+ <ResizableItem theme={theme} size="280px" min="220px" max="420px" collapsed={collapsed}>
63
+ <Sidebar
64
+ theme={theme}
65
+ conversations={conversations}
66
+ activeId={activeId}
67
+ collapsed={collapsed}
68
+ onSelect={selectConversation}
69
+ onNewChat={newChat}
70
+ onToggle={() => setCollapsed((c) => !c)}
71
+ />
72
+ </ResizableItem>
73
+
74
+ <ResizableItem theme={theme}>
75
+ <main className="main">
76
+ <header className="bar">
77
+ <div className="bar-left">
78
+ {collapsed && (
79
+ <Button
80
+ theme={theme}
81
+ variant="ghost"
82
+ size="icon"
83
+ icon="panel-left"
84
+ label="Show sidebar"
85
+ onClick={() => setCollapsed(false)}
86
+ />
87
+ )}
88
+ <span className="brand">@kitn.ai/ui · composed chat</span>
89
+ </div>
90
+ <ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />
91
+ </header>
92
+
93
+ <ThreadView theme={theme} messages={chat.messages} />
94
+
95
+ <Composer
96
+ theme={theme}
97
+ loading={chat.loading}
98
+ suggestions={chat.messages.length <= 1 ? SUGGESTIONS : []}
99
+ triggers={TRIGGERS}
100
+ onSubmit={send}
101
+ onSuggestionClick={send}
102
+ />
103
+ </main>
104
+ </ResizableItem>
105
+ </Resizable>
106
+ </div>
107
+ );
108
+ }
@@ -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-message>`, 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: "Map your `messages` to `<Message>` elements inside a scroll area, put a `<PromptInput>` below, and wire `onSubmit` to append + stream. That's exactly what this example does — read `App.tsx`." }] },
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
+ // <PromptInput> 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 import the wrappers + tokens:\n\n```tsx\nimport { Conversations, Message, PromptInput } from '@kitn.ai/ui/react';\nimport '@kitn.ai/ui/theme.tokens.css';\n```",
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**: `<Conversations>` for the sidebar, a `<Message>` per item in a scroll area, and `<PromptInput>` at the bottom — all wired with React 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,87 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import { PromptInput } from '@kitn.ai/ui/react';
3
+ import type { PromptInputProps } from '@kitn.ai/ui/react';
4
+ import type { Theme } from '../App';
5
+ import { useVoiceInput } from '@kitn.ai/ui/react';
6
+
7
+ interface ComposerProps {
8
+ theme: Theme;
9
+ loading: boolean;
10
+ suggestions: string[];
11
+ triggers: PromptInputProps['triggers'];
12
+ onSubmit: (value: string) => void;
13
+ onSuggestionClick: (value: string) => void;
14
+ }
15
+
16
+ /**
17
+ * The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
18
+ * the element ref, a live text mirror, clear-on-submit, and seeding voice.
19
+ *
20
+ * The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
21
+ * caret. Assigning a plain STRING `value` flips the element into controlled mode,
22
+ * which re-applies the property and collapses the shadow-DOM selection — that
23
+ * disables the caret-anchored menu. So we NEVER push a string: clear-on-submit goes
24
+ * through the element's `clear()` method, and voice seeds a ComposerDoc (a
25
+ * non-string value is a one-time seed that leaves the input uncontrolled). We keep
26
+ * the live text mirrored into `textRef` (from `onValueChange`) so voice can append.
27
+ */
28
+ export function Composer({ theme, loading, suggestions, triggers, onSubmit, onSuggestionClick }: ComposerProps) {
29
+ // Ref to the underlying <kai-prompt-input> element for the imperative pushes
30
+ // (clear() on submit, ComposerDoc seed from voice); textRef mirrors the live text.
31
+ const promptRef = useRef<HTMLElement>(null);
32
+ const textRef = useRef('');
33
+
34
+ // Voice: on a transcript, append it to the current text and seed the
35
+ // (uncontrolled) input as a ComposerDoc. A non-string `value` is a one-time seed
36
+ // into the element's internal state; a plain STRING would instead flip it into
37
+ // controlled mode and re-break the `/`+`@` triggers and submit.
38
+ const { supported: voiceSupported, start: startVoice } = useVoiceInput((transcript) => {
39
+ const next = (textRef.current ? textRef.current + ' ' : '') + transcript;
40
+ textRef.current = next;
41
+ const el = promptRef.current as (HTMLElement & { value?: unknown }) | null;
42
+ if (el) el.value = [{ type: 'text', text: next }];
43
+ });
44
+
45
+ const handleVoice = useCallback(() => {
46
+ // Mic is Chromium-only; degrade gracefully everywhere else.
47
+ if (!voiceSupported) {
48
+ alert('Voice input needs a Chromium browser.');
49
+ return;
50
+ }
51
+ startVoice();
52
+ }, [voiceSupported, startVoice]);
53
+
54
+ const handleSubmit = useCallback(
55
+ (value: string) => {
56
+ // Clear our own uncontrolled input first — reset the mirror + call the
57
+ // element's clear() method (uncontrolled-safe, it fires kai-value-change
58
+ // internally; never a string assignment) — then hand the text up. The parent
59
+ // owns append + streaming.
60
+ textRef.current = '';
61
+ (promptRef.current as (HTMLElement & { clear?: () => void }) | null)?.clear?.();
62
+ onSubmit(value);
63
+ },
64
+ [onSubmit],
65
+ );
66
+
67
+ return (
68
+ <div className="composer">
69
+ <PromptInput
70
+ ref={promptRef}
71
+ theme={theme}
72
+ placeholder="Message the demo…"
73
+ loading={loading}
74
+ suggestions={suggestions}
75
+ triggers={triggers}
76
+ voice
77
+ onVoice={handleVoice}
78
+ onValueChange={(e) => { textRef.current = e.detail.value; }}
79
+ onSubmit={(e) => handleSubmit(e.detail.value)}
80
+ onSuggestionClick={(e) => onSuggestionClick(e.detail.value)}
81
+ />
82
+ <p className="composer-hint">
83
+ Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
84
+ </p>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,38 @@
1
+ import { Conversations } from '@kitn.ai/ui/react';
2
+ import type { Theme } from '../App';
3
+ import type { Conversation } from '../chat-data';
4
+
5
+ interface SidebarProps {
6
+ theme: Theme;
7
+ conversations: Conversation[];
8
+ activeId: string;
9
+ collapsed: boolean;
10
+ onSelect: (id: string) => void;
11
+ onNewChat: () => void;
12
+ onToggle: () => void;
13
+ }
14
+
15
+ /**
16
+ * The conversation rail — a thin wrapper over `<kai-conversations>`. The `.sidebar` div
17
+ * owns the shell's right border (kept OFF the element so it follows the shell's
18
+ * light/dark tokens, not the light-mode ones the element pins on itself at `:host`).
19
+ * The rail's `collapsed` is CONTROLLED by the app's collapsed state, so it stays in
20
+ * sync with the parent `<ResizableItem collapsed>` and re-expands the list on restore;
21
+ * `onToggleSidebar` still reports the toggle intent up via `onToggle`.
22
+ */
23
+ export function Sidebar({ theme, conversations, activeId, collapsed, onSelect, onNewChat, onToggle }: SidebarProps) {
24
+ return (
25
+ <aside className="sidebar">
26
+ <Conversations
27
+ theme={theme}
28
+ groups={[]} /* flat list: the element buckets `conversations` by recency itself */
29
+ conversations={conversations}
30
+ activeId={activeId}
31
+ collapsed={collapsed}
32
+ onConversationSelect={(e) => onSelect(e.detail.id)}
33
+ onNewChat={onNewChat}
34
+ onToggleSidebar={onToggle}
35
+ />
36
+ </aside>
37
+ );
38
+ }
@@ -0,0 +1,22 @@
1
+ import { Button } from '@kitn.ai/ui/react';
2
+ import type { Theme } from '../App';
3
+ import { MoonIcon, SunIcon } from './icons';
4
+
5
+ interface ThemeToggleProps {
6
+ theme: Theme;
7
+ onToggle: () => void;
8
+ }
9
+
10
+ /**
11
+ * Light/dark switch for the top bar. Shows the moon in light mode (tap → dark) and
12
+ * the sun in dark mode. The `<Button>` is icon-only, so it carries the accessible
13
+ * label; the glyph is decorative (`aria-hidden`) and sits in the `icon` slot.
14
+ */
15
+ export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
16
+ const Glyph = theme === 'light' ? MoonIcon : SunIcon;
17
+ return (
18
+ <Button theme={theme} variant="ghost" size="icon" label="Toggle light/dark theme" onClick={onToggle}>
19
+ <Glyph slot="icon" aria-hidden />
20
+ </Button>
21
+ );
22
+ }
@@ -0,0 +1,40 @@
1
+ import { Thread } from '@kitn.ai/ui/react';
2
+ import type { ChatMessage } from '@kitn.ai/ui/react';
3
+ import { partsToText } from '@kitn.ai/ui/state';
4
+ import type { Theme } from '../App';
5
+
6
+ interface ThreadViewProps {
7
+ theme: Theme;
8
+ messages: ChatMessage[];
9
+ }
10
+
11
+ /**
12
+ * The scrolling message list. `<kai-thread>` (React `<Thread>`) owns the message
13
+ * rendering, the centered fixed-width column, and stick-to-bottom scroll, so this
14
+ * component just bakes the per-message actions onto the assistant turns and wires
15
+ * the custom `speak` action to the browser's speech synthesis. `copy` (and the
16
+ * feedback votes) are handled inside the element.
17
+ */
18
+ export function ThreadView({ theme, messages }: ThreadViewProps) {
19
+ // <Thread> reads `actions` off each message; only assistant replies get them.
20
+ const withActions: ChatMessage[] = messages.map((m) =>
21
+ m.role === 'assistant'
22
+ ? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
23
+ : m,
24
+ );
25
+ return (
26
+ <Thread
27
+ className="thread"
28
+ theme={theme}
29
+ messages={withActions}
30
+ onMessageAction={(e) => {
31
+ if (e.detail.action === 'speak') {
32
+ const m = messages.find((x) => x.id === e.detail.messageId);
33
+ if (!m) return;
34
+ window.speechSynthesis.cancel();
35
+ window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
36
+ }
37
+ }}
38
+ />
39
+ );
40
+ }
@@ -0,0 +1,25 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ /**
4
+ * Moon glyph — shown in light mode (tap the toggle → dark). moon/sun aren't in the
5
+ * kit's `<Icon>` set, so the example owns them. Props spread straight onto the
6
+ * `<svg>`, so `slot="icon"`, `className`, and `aria-hidden` pass through and it
7
+ * drops into `<Button>`'s icon slot.
8
+ */
9
+ export function MoonIcon({ size = 20, ...props }: { size?: number } & SVGProps<SVGSVGElement>) {
10
+ return (
11
+ <svg
12
+ viewBox="0 0 24 24"
13
+ width={size}
14
+ height={size}
15
+ fill="none"
16
+ stroke="currentColor"
17
+ strokeWidth={2}
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ {...props}
21
+ >
22
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
23
+ </svg>
24
+ );
25
+ }
@@ -0,0 +1,25 @@
1
+ import type { SVGProps } from 'react';
2
+
3
+ /**
4
+ * Sun glyph — shown in dark mode (tap the toggle → light). See `MoonIcon` for why
5
+ * the example owns these icons. Props spread straight onto the `<svg>`.
6
+ */
7
+ export function SunIcon({ size = 20, ...props }: { size?: number } & SVGProps<SVGSVGElement>) {
8
+ return (
9
+ <svg
10
+ viewBox="0 0 24 24"
11
+ width={size}
12
+ height={size}
13
+ fill="none"
14
+ stroke="currentColor"
15
+ strokeWidth={2}
16
+ strokeLinecap="round"
17
+ strokeLinejoin="round"
18
+ {...props}
19
+ >
20
+ <circle cx="12" cy="12" r="4" />
21
+ <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" />
22
+ <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" />
23
+ </svg>
24
+ );
25
+ }
@@ -0,0 +1,2 @@
1
+ export { MoonIcon } from './MoonIcon';
2
+ export { SunIcon } from './SunIcon';
@@ -0,0 +1 @@
1
+ export { useConversations } from './useConversations';
@@ -0,0 +1,37 @@
1
+ import { useCallback, useState } from 'react';
2
+ import type { KaiChatController } from '@kitn.ai/ui/react';
3
+ import { THREADS, newId, type Conversation } from '../chat-data';
4
+
5
+ /**
6
+ * Owns the conversation list + which one is active, plus the in-memory `THREADS`
7
+ * stash: on select/new we swap the OPEN thread out to `THREADS` and swap the picked
8
+ * one in. Takes the `useKaiChat` controller so it can read the live messages when
9
+ * stashing and reset them when loading another thread.
10
+ */
11
+ export function useConversations(chat: KaiChatController, initial: Conversation[]) {
12
+ const [conversations, setConversations] = useState<Conversation[]>(initial);
13
+ const [activeId, setActiveId] = useState(initial[0].id);
14
+
15
+ const selectConversation = useCallback(
16
+ (id: string) => {
17
+ THREADS[activeId] = chat.messages; // stash the open thread
18
+ setActiveId(id);
19
+ chat.setMessages(THREADS[id] ?? []); // load the picked one
20
+ },
21
+ [activeId, chat],
22
+ );
23
+
24
+ const newChat = useCallback(() => {
25
+ const id = newId();
26
+ THREADS[activeId] = chat.messages;
27
+ THREADS[id] = [];
28
+ setConversations((cs) => [
29
+ { id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
30
+ ...cs,
31
+ ]);
32
+ setActiveId(id);
33
+ chat.setMessages([]);
34
+ }, [activeId, chat]);
35
+
36
+ return { conversations, activeId, selectConversation, newChat };
37
+ }