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,47 @@
1
+ import { signal, type WritableSignal } from '@angular/core';
2
+ import type { ChatMessage } from '@kitn.ai/ui';
3
+ import {
4
+ appendMessage,
5
+ createAssistantStream,
6
+ onStreamSettled,
7
+ type SetMessages,
8
+ type AssistantStream,
9
+ } from '@kitn.ai/ui/state';
10
+
11
+ /**
12
+ * Angular port of the kit's `useKaiChat` (a React hook), built on the SAME
13
+ * framework-neutral state core (`@kitn.ai/ui/state`) the React hook uses. It owns
14
+ * the message array + the `loading` flag and exposes ergonomic ops. Swap
15
+ * `mockResponse(text)` for a real `fetch` to ship a real app.
16
+ *
17
+ * `messages` is a plain Angular signal: every op assigns a BRAND-NEW array
18
+ * reference (`messages.set(updater(prev))`), which is exactly what re-renders
19
+ * `<kai-thread>` when the template re-binds `[messages]` — mutating the array in
20
+ * place does not. Signals also keep the array plain (no proxy), so what reaches the
21
+ * element's `messages` property is a clean `ChatMessage[]`.
22
+ */
23
+ export interface ChatStore {
24
+ messages: WritableSignal<ChatMessage[]>;
25
+ loading: WritableSignal<boolean>;
26
+ append: (msg: ChatMessage) => void;
27
+ setMessages: (next: ChatMessage[]) => void;
28
+ streamAssistant: (init?: Partial<ChatMessage>) => AssistantStream;
29
+ }
30
+
31
+ export function createChat(initialMessages: ChatMessage[] = []): ChatStore {
32
+ const messages = signal<ChatMessage[]>([...initialMessages]);
33
+ const loading = signal(false);
34
+
35
+ // The one universal contract the state core drives: a functional-updater setter
36
+ // (React setState shape) that assigns a fresh array each time.
37
+ const set: SetMessages = (updater) => messages.set(updater(messages()));
38
+
39
+ const append = (msg: ChatMessage) => set((prev) => appendMessage(prev, msg));
40
+ const setMessages = (next: ChatMessage[]) => messages.set([...next]);
41
+ const streamAssistant = (init?: Partial<ChatMessage>): AssistantStream => {
42
+ loading.set(true);
43
+ return onStreamSettled(createAssistantStream(set, init), () => loading.set(false));
44
+ };
45
+
46
+ return { messages, loading, append, setMessages, streamAssistant };
47
+ }
@@ -0,0 +1,42 @@
1
+ import { signal, type WritableSignal } from '@angular/core';
2
+ import { THREADS, newId, type Conversation } from '../../chat-data';
3
+ import type { ChatStore } from './chat.store';
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 `createChat` store so it can read the live messages when
9
+ * stashing and reset them when loading another thread. Mirrors the Vue/React
10
+ * `useConversations`.
11
+ */
12
+ export interface ConversationsStore {
13
+ conversations: WritableSignal<Conversation[]>;
14
+ activeId: WritableSignal<string>;
15
+ selectConversation: (id: string) => void;
16
+ newChat: () => void;
17
+ }
18
+
19
+ export function createConversations(chat: ChatStore, initial: Conversation[]): ConversationsStore {
20
+ const conversations = signal<Conversation[]>([...initial]);
21
+ const activeId = signal(initial[0].id);
22
+
23
+ const selectConversation = (id: string) => {
24
+ THREADS[activeId()] = chat.messages(); // stash the open thread
25
+ activeId.set(id);
26
+ chat.setMessages(THREADS[id] ?? []); // load the picked one
27
+ };
28
+
29
+ const newChat = () => {
30
+ const id = newId();
31
+ THREADS[activeId()] = chat.messages();
32
+ THREADS[id] = [];
33
+ conversations.set([
34
+ { id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
35
+ ...conversations(),
36
+ ]);
37
+ activeId.set(id);
38
+ chat.setMessages([]);
39
+ };
40
+
41
+ return { conversations, activeId, selectConversation, newChat };
42
+ }
@@ -0,0 +1,57 @@
1
+ /** Minimal shape of a Web Speech recognition result event — enough to read the
2
+ * first transcript. The full SpeechRecognition API isn't in TS's lib.dom yet. */
3
+ interface SpeechResultEvent {
4
+ results: ArrayLike<ArrayLike<{ transcript: string }>>;
5
+ }
6
+ interface SpeechRecognitionLike {
7
+ lang: string;
8
+ interimResults: boolean;
9
+ onresult: ((event: SpeechResultEvent) => void) | null;
10
+ start(): void;
11
+ }
12
+ type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
13
+
14
+ /** Resolve the Chromium (`webkitSpeechRecognition`) or standard constructor, if any. */
15
+ function getSpeechRecognition(): SpeechRecognitionCtor | undefined {
16
+ if (typeof window === 'undefined') return undefined;
17
+ const w = window as unknown as {
18
+ SpeechRecognition?: SpeechRecognitionCtor;
19
+ webkitSpeechRecognition?: SpeechRecognitionCtor;
20
+ };
21
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition;
22
+ }
23
+
24
+ /**
25
+ * Web Speech mic → one-shot transcript. Chromium-only (`webkitSpeechRecognition`);
26
+ * `supported` is false elsewhere so the caller can degrade gracefully. Vue port of
27
+ * the kit's `useVoiceInput` (a React hook). It hands the raw transcript to
28
+ * `onTranscript` and lets the caller decide what to do with it (the composer mic
29
+ * appends it to the live text). No composer or DOM knowledge.
30
+ *
31
+ * @param onTranscript called with the final recognized transcript.
32
+ * @returns `supported` (Chromium-only availability) and a `start` that begins one
33
+ * recognition pass (a no-op when unsupported).
34
+ */
35
+ export function useVoiceInput(onTranscript: (transcript: string) => void): {
36
+ supported: boolean;
37
+ start: () => void;
38
+ } {
39
+ // Latest-callback ref: keeps `start` stable while always calling the current
40
+ // `onTranscript`, mirroring the React hook (no stale closure).
41
+ const cbRef = { current: onTranscript };
42
+ cbRef.current = onTranscript;
43
+
44
+ const supported = getSpeechRecognition() !== undefined;
45
+
46
+ const start = () => {
47
+ const Recognition = getSpeechRecognition();
48
+ if (!Recognition) return;
49
+ const rec = new Recognition();
50
+ rec.lang = 'en-US';
51
+ rec.interimResults = false;
52
+ rec.onresult = (event) => cbRef.current(event.results[0][0].transcript);
53
+ rec.start();
54
+ };
55
+
56
+ return { supported, start };
57
+ }
@@ -0,0 +1,2 @@
1
+ // Shared across the example's components. Mirrors the Vue reference's `types.ts`.
2
+ export type Theme = 'light' | 'dark';
@@ -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.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 = this.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,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>@kitn.ai/ui Angular example</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body, app-root { height: 100%; }
10
+ app-root { display: block; }
11
+ </style>
12
+ </head>
13
+ <body>
14
+ <app-root></app-root>
15
+ </body>
16
+ </html>
@@ -0,0 +1,16 @@
1
+ import '@kitn.ai/ui/elements'; // registers the kai-* custom elements (async)
2
+ import { bootstrapApplication } from '@angular/platform-browser';
3
+ import { App } from './app/app';
4
+ import { appConfig } from './app/app.config';
5
+
6
+ // The kai-* elements register ASYNCHRONOUSLY. Angular sets array/object DOM
7
+ // properties ([conversations], [messages], [triggers], …) the moment it stamps a
8
+ // <kai-*> tag; if that happens before the element upgrades, the property write hits
9
+ // a plain, not-yet-upgraded node and is clobbered by the element's empty defaults on
10
+ // upgrade. Raw web-component consumers have no upgrade-race guard (unlike the React
11
+ // wrappers), so we gate bootstrap on `whenDefined` for every tag we use — Angular
12
+ // then binds against UPGRADED elements. Attributes survive upgrade; properties do not.
13
+ const TAGS = ['kai-resizable', 'kai-resizable-item', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-button'];
14
+ Promise.all(TAGS.map((t) => customElements.whenDefined(t)))
15
+ .then(() => bootstrapApplication(App, appConfig))
16
+ .catch((err) => console.error(err));
@@ -0,0 +1,10 @@
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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist/out-tsc/app",
5
+ "types": []
6
+ },
7
+ "files": ["src/main.ts"],
8
+ "include": ["src/**/*.d.ts"]
9
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "noImplicitOverride": true,
6
+ "noImplicitReturns": true,
7
+ "noFallthroughCasesInSwitch": true,
8
+ "skipLibCheck": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "experimentalDecorators": true,
12
+ "importHelpers": true,
13
+ "target": "ES2022",
14
+ "module": "preserve"
15
+ },
16
+ "angularCompilerOptions": {
17
+ "enableI18nLegacyMessageIdFormat": false,
18
+ "strictInjectionParameters": true,
19
+ "strictInputAccessModifiers": true,
20
+ "strictTemplates": true
21
+ }
22
+ }
@@ -0,0 +1,11 @@
1
+ node_modules/
2
+ .next/
3
+ out/
4
+ .env
5
+ .env.local
6
+ *.log
7
+ .DS_Store
8
+ next-env.d.ts
9
+ *.tsbuildinfo
10
+ vendor/
11
+ *.tgz
@@ -0,0 +1,125 @@
1
+ // Self-contained sample data + the kit's shared mock responder — no backend.
2
+ //
3
+ // THIS MODULE IS EVALUATED TWICE: once on the server while the page is being
4
+ // prerendered, and once in the browser. Everything in it is therefore plain data
5
+ // and pure functions — nothing here touches `window`, `document` or
6
+ // `customElements`, which is the rule for any module a Server Component's tree
7
+ // reaches, client island or not.
8
+ //
9
+ // The date helper below is the one thing that produces a DIFFERENT value on each
10
+ // of those two evaluations, and it is safe for a specific reason worth knowing:
11
+ // `CONVERSATIONS` is only ever handed to `<kai-conversations>` as a JS PROPERTY,
12
+ // and the React wrappers assign properties in a layout effect, which never runs
13
+ // on the server. So no timestamp reaches the server-rendered HTML and there is
14
+ // nothing for hydration to compare. Render one of these into JSX and that stops
15
+ // being true — that is the shape of a real hydration mismatch.
16
+ import type { MessagePart } from '@kitn.ai/ui';
17
+ import { createMockResponder } from '@kitn.ai/ui/state';
18
+
19
+ export interface Conversation {
20
+ id: string;
21
+ title: string;
22
+ scope: { type: 'document' | 'collection' };
23
+ messageCount: number;
24
+ lastMessageAt: string;
25
+ updatedAt: string;
26
+ }
27
+
28
+ export interface Msg {
29
+ id: string;
30
+ role: 'user' | 'assistant';
31
+ parts: MessagePart[];
32
+ }
33
+
34
+ export function newId(): string {
35
+ return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
36
+ ? crypto.randomUUID()
37
+ : 'id-' + Math.random().toString(36).slice(2);
38
+ }
39
+
40
+ const iso = (daysAgo = 0) => new Date(Date.now() - daysAgo * 86_400_000).toISOString();
41
+
42
+ export const CONVERSATIONS: Conversation[] = [
43
+ { id: 'c1', title: 'Server components', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(0), updatedAt: iso(0) },
44
+ { id: 'c2', title: 'Composing your own chat', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(0), updatedAt: iso(0) },
45
+ { id: 'c3', title: 'Theming + dark mode', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(1), updatedAt: iso(1) },
46
+ ];
47
+
48
+ export const THREADS: Record<string, Msg[]> = {
49
+ c1: [
50
+ { id: 'c1u', role: 'user', parts: [{ type: 'text', text: 'Where does the "use client" directive go, and how do the kai-* elements survive the App Router?' }] },
51
+ { id: 'c1a', role: 'assistant', parts: [{ type: 'text', text: "`app/layout.tsx` and `app/page.tsx` are **Server Components** — neither carries a directive. `app/workspace.tsx` does, because IT uses hooks and event handlers. That is the standard RSC rule, not a kit requirement: the React wrappers ship their own `'use client'` banner, so a Server Component can render one directly.\n\nThe elements themselves server-render as **bare tags**. Each wrapper registers its element inside a layout effect, which never runs on the server, so the prerendered HTML contains `<kai-thread></kai-thread>` and nothing inside it. After hydration the wrapper defines the element and assigns `messages` as a live JS **property** — arrays go across unstringified, so nothing has to be serialized into an attribute.\n\nView source on this page and you will find the empty tags. Then watch this reply stream in: that is the same property channel still working, one new array reference per chunk." }] },
52
+ ],
53
+ c2: [
54
+ { id: 'c2u', role: 'user', parts: [{ type: 'text', text: 'How do I build my own chat instead of dropping in `<kai-chat>`?' }] },
55
+ { id: 'c2a', role: 'assistant', parts: [{ type: 'text', text: "Put a `<Thread>` in a scroll area, a `<PromptInput>` below it, and let `useKaiChat` own the message array and the streaming. That's exactly what this app does — read `app/workspace.tsx`." }] },
56
+ ],
57
+ c3: [
58
+ { id: 'c3u', role: 'user', parts: [{ type: 'text', text: 'How does dark mode work?' }] },
59
+ { id: 'c3a', role: 'assistant', parts: [{ type: 'text', text: "Each element takes a `theme` prop (`light` / `dark` / `auto`). Drive it from client state — toggle it with the button top-right. The kit's `--color-*` tokens flip under a `.dark` class for your own surrounding chrome." }] },
60
+ ],
61
+ };
62
+
63
+ export const SUGGESTIONS = [
64
+ 'Where does "use client" go?',
65
+ 'How do I install it?',
66
+ 'Show me some markdown',
67
+ ];
68
+
69
+ // Rich entity triggers for the prompt input: typing `/` opens the skills menu,
70
+ // `@` opens the agents menu. Each selection inserts an atomic pill. Set on
71
+ // <PromptInput> as the `triggers` JS property (a TriggerDef[]).
72
+ export const TRIGGERS = [
73
+ {
74
+ char: '/',
75
+ kind: 'skill',
76
+ items: [
77
+ { id: 'summarize', label: 'Summarize', description: 'Summarize the thread', promptText: 'Summarize the thread.' },
78
+ { id: 'translate', label: 'Translate', description: 'Translate to English', promptText: 'Translate to English.' },
79
+ { id: 'rewrite', label: 'Rewrite', description: 'Rewrite for clarity', promptText: 'Rewrite this for clarity.' },
80
+ ],
81
+ },
82
+ {
83
+ char: '@',
84
+ kind: 'agent',
85
+ items: [
86
+ { id: 'researcher', label: 'Researcher', description: 'Deep web research', group: 'Agents' },
87
+ { id: 'coder', label: 'Coder', description: 'Writes and edits code', group: 'Agents' },
88
+ { id: 'designer', label: 'Designer', description: 'UI and visual design', group: 'Agents' },
89
+ ],
90
+ },
91
+ ];
92
+
93
+ // The replies the mock cycles through, one per turn. Markdown-rich on purpose:
94
+ // the thread renders markdown, code blocks and blockquotes, so the canned replies
95
+ // exercise that instead of only proving text arrives.
96
+ //
97
+ // The first one says it is a mock in words. That is the WEAKEST of the responder's
98
+ // tells and the only one a real model could imitate, which is exactly why it is
99
+ // not the only one — the rest are on the wire, below.
100
+ const MOCK_REPLIES = [
101
+ "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 route handler) to ship a real app.",
102
+ "Install it:\n\n```bash\nnpm install @kitn.ai/ui\n```\n\nThen import the wrappers + tokens:\n\n```tsx\nimport { Thread, PromptInput } from '@kitn.ai/ui/react';\nimport '@kitn.ai/ui/theme.tokens.css';\n```",
103
+ "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.",
104
+ "Nothing about this thread came from the server. Next prerendered the **empty** `<kai-thread>` tag, the browser hydrated it, and every chunk you are reading now arrived as a new `messages` array assigned to that element as a JS property.",
105
+ ];
106
+
107
+ /**
108
+ * The kit's own mock responder, shared with the `kai` MCP scaffolder and
109
+ * `create-kai` so there is ONE implementation of this and not eight.
110
+ *
111
+ * It yields canned SSE frames in the OpenAI chat-completions shape, which
112
+ * `readOpenAIStream` parses exactly as it parses a real provider's. So this
113
+ * no-backend preview runs the kit's REAL streaming path — the SSE reader, the
114
+ * part folding, all of it — rather than a hand-rolled loop that merely resembles
115
+ * it. Going live then changes one expression, not the handler.
116
+ *
117
+ * It also cannot be mistaken for a real turn: the stream opens with a `: kai-mock`
118
+ * SSE comment, every frame carries a `_kai_mock` field, `model` reports as
119
+ * `kai-mock` (no provider serves that, so an echoed mock frame is rejected
120
+ * upstream rather than quietly believed), and usage is all zeros.
121
+ *
122
+ * MODULE scope, not per-send: the responder owns the cursor into the replies
123
+ * above, so rebuilding it each turn would answer with the first one forever.
124
+ */
125
+ export const mockResponse = createMockResponder({ replies: MOCK_REPLIES });
@@ -0,0 +1,88 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import { PromptInput, useVoiceInput } from '@kitn.ai/ui/react';
3
+ import type { PromptInputProps } from '@kitn.ai/ui/react';
4
+ import type { Theme } from '../theme';
5
+
6
+ interface ComposerProps {
7
+ theme: Theme;
8
+ loading: boolean;
9
+ suggestions: string[];
10
+ triggers: PromptInputProps['triggers'];
11
+ onSubmit: (value: string) => void;
12
+ onSuggestionClick: (value: string) => void;
13
+ }
14
+
15
+ /**
16
+ * The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
17
+ * the element ref, a live text mirror, clear-on-submit, and seeding voice.
18
+ *
19
+ * The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
20
+ * caret. Assigning a plain STRING `value` flips the element into controlled mode,
21
+ * which re-applies the property and collapses the shadow-DOM selection — that
22
+ * disables the caret-anchored menu. So we NEVER push a string: clear-on-submit
23
+ * goes through the element's `clear()` method, and voice seeds a ComposerDoc (a
24
+ * non-string value is a one-time seed that leaves the input uncontrolled). We keep
25
+ * the live text mirrored into `textRef` (from `onValueChange`) so voice can append.
26
+ *
27
+ * SERVER-RENDERING NOTE. `useVoiceInput` reports `supported: false` on the server
28
+ * (there is no `window.SpeechRecognition` there) and true in Chromium, so that
29
+ * flag is read ONLY inside the click handler and never during render. Branching
30
+ * render output on it would put a different tree in the prerendered HTML than the
31
+ * client builds on hydration, which is the textbook hydration mismatch — and one
32
+ * `next build` would minify into a numbered React error rather than explain.
33
+ */
34
+ export function Composer({ theme, loading, suggestions, triggers, onSubmit, onSuggestionClick }: ComposerProps) {
35
+ // Ref to the underlying <kai-prompt-input> element for the imperative pushes
36
+ // (clear() on submit, ComposerDoc seed from voice); textRef mirrors the live text.
37
+ const promptRef = useRef<HTMLElement>(null);
38
+ const textRef = useRef('');
39
+
40
+ const { supported: voiceSupported, start: startVoice } = useVoiceInput((transcript) => {
41
+ const next = (textRef.current ? textRef.current + ' ' : '') + transcript;
42
+ textRef.current = next;
43
+ const el = promptRef.current as (HTMLElement & { value?: unknown }) | null;
44
+ if (el) el.value = [{ type: 'text', text: next }];
45
+ });
46
+
47
+ const handleVoice = useCallback(() => {
48
+ // Mic is Chromium-only; degrade gracefully everywhere else.
49
+ if (!voiceSupported) {
50
+ alert('Voice input needs a Chromium browser.');
51
+ return;
52
+ }
53
+ startVoice();
54
+ }, [voiceSupported, startVoice]);
55
+
56
+ const handleSubmit = useCallback(
57
+ (value: string) => {
58
+ // Clear our own uncontrolled input first — reset the mirror + call the
59
+ // element's clear() method (uncontrolled-safe; never a string assignment) —
60
+ // then hand the text up. The workspace owns append + streaming.
61
+ textRef.current = '';
62
+ (promptRef.current as (HTMLElement & { clear?: () => void }) | null)?.clear?.();
63
+ onSubmit(value);
64
+ },
65
+ [onSubmit],
66
+ );
67
+
68
+ return (
69
+ <div className="composer">
70
+ <PromptInput
71
+ ref={promptRef}
72
+ theme={theme}
73
+ placeholder="Message the demo…"
74
+ loading={loading}
75
+ suggestions={suggestions}
76
+ triggers={triggers}
77
+ voice
78
+ onVoice={handleVoice}
79
+ onValueChange={(e) => { textRef.current = e.detail.value; }}
80
+ onSubmit={(e) => handleSubmit(e.detail.value)}
81
+ onSuggestionClick={(e) => onSuggestionClick(e.detail.value)}
82
+ />
83
+ <p className="composer-hint">
84
+ Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
85
+ </p>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,68 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ /**
4
+ * THE SERVER-RENDERING PROOF, ON THE PAGE.
5
+ *
6
+ * This starter's original job was to show that a `kai-*` element survives Next's
7
+ * prerender + hydration, and this badge is what makes that visible without opening
8
+ * devtools. It is worth keeping alongside the working chat because HYDRATION
9
+ * FAILURE LOOKS LIKE SUCCESS: Next can emit perfectly correct HTML and then throw
10
+ * during hydration, leaving a page that reads exactly right and does nothing. A
11
+ * screenshot cannot tell those apart. This badge can, because the only way it
12
+ * turns green is for client JavaScript to have run.
13
+ *
14
+ * IT IS WORTH MORE ON NEXT THAN ANYWHERE ELSE, because a production React build
15
+ * MINIFIES hydration errors into a numbered link — so `next build && next start`
16
+ * shows you a silently dead page, and only `next dev` logs the mismatch in words.
17
+ * The badge is the same signal without the dev-only console.
18
+ *
19
+ * HOW IT AVOIDS BEING THE BUG IT REPORTS. The state starts `null` — the same value
20
+ * on the server and on React's first client render — so the two trees agree and
21
+ * hydration has nothing to reconcile. Only afterwards does the effect (which never
22
+ * runs on the server) look at `customElements` and re-render. Reading
23
+ * `customElements` during render instead would produce "none" in the server HTML
24
+ * and "5" in the client tree, i.e. a hydration mismatch inside the hydration
25
+ * detector.
26
+ *
27
+ * The tags are polled on a rAF loop rather than checked once: each React wrapper
28
+ * registers its own element from a layout effect and the element modules load as
29
+ * dynamic imports, so the definitions land over the next few frames rather than
30
+ * all at once.
31
+ */
32
+ const TAGS = ['kai-button', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-resizable'] as const;
33
+
34
+ export function HydrationBadge() {
35
+ const [defined, setDefined] = useState<number | null>(null);
36
+
37
+ useEffect(() => {
38
+ let raf = 0;
39
+ const check = () => {
40
+ const n = TAGS.filter((t) => !!customElements.get(t)).length;
41
+ setDefined(n);
42
+ if (n < TAGS.length) raf = requestAnimationFrame(check);
43
+ };
44
+ check();
45
+ return () => cancelAnimationFrame(raf);
46
+ }, []);
47
+
48
+ const hydrated = defined === TAGS.length;
49
+ return (
50
+ <span
51
+ className="hydration"
52
+ data-testid="registration-status"
53
+ data-state={hydrated ? 'hydrated' : 'server'}
54
+ title={
55
+ hydrated
56
+ ? `Client JS ran: all ${TAGS.length} kai-* elements are defined in customElements.`
57
+ : 'Server-rendered markup. The kai-* tags are still bare — no client JS has registered them yet.'
58
+ }
59
+ >
60
+ <span className="hydration-dot" aria-hidden />
61
+ {defined === null
62
+ ? 'server-rendered · elements not yet registered'
63
+ : hydrated
64
+ ? `hydrated · ${defined}/${TAGS.length} kai-* elements registered`
65
+ : `hydrating · ${defined}/${TAGS.length} kai-* elements registered`}
66
+ </span>
67
+ );
68
+ }