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,96 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue';
3
+ import type { KaiPromptInputElementProps } from '@kitn.ai/ui/elements';
4
+ import type { Theme } from '../types';
5
+ import { useVoiceInput } from '../composables';
6
+
7
+ /**
8
+ * The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
9
+ * the element ref, a live text mirror, clear-on-submit, and seeding voice.
10
+ *
11
+ * The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
12
+ * caret. We NEVER bind `:value` with a plain STRING: that flips the element into
13
+ * controlled mode, which re-applies the property and collapses the shadow-DOM
14
+ * selection, disabling the caret-anchored menu. So clear-on-submit goes through
15
+ * the element's `clear()` method, and voice seeds a ComposerDoc (a non-string
16
+ * value is a one-time seed that leaves the input uncontrolled). We keep the live
17
+ * text mirrored in `liveText` (from `kai-value-change`) so voice can append.
18
+ */
19
+ defineProps<{
20
+ theme: Theme;
21
+ loading: boolean;
22
+ suggestions: string[];
23
+ // The kit owns this shape. Take it straight off the element's own prop type
24
+ // (`@kitn.ai/ui/elements`) instead of re-declaring it here, so a change to the
25
+ // trigger contract shows up as a build error rather than silent drift.
26
+ triggers: NonNullable<KaiPromptInputElementProps['triggers']>;
27
+ }>();
28
+
29
+ const emit = defineEmits<{
30
+ submit: [value: string];
31
+ suggestion: [value: string];
32
+ }>();
33
+
34
+ // Ref to the underlying <kai-prompt-input> element for the imperative pushes
35
+ // (clear() on submit, ComposerDoc seed from voice); `liveText` mirrors the text.
36
+ const promptRef = ref<HTMLElement | null>(null);
37
+ let liveText = '';
38
+
39
+ // Voice: on a transcript, append it to the current text and seed the (uncontrolled)
40
+ // input as a ComposerDoc. A non-string `value` is a one-time seed into the
41
+ // element's internal state; a plain STRING would instead flip it into controlled
42
+ // mode and re-break the `/`+`@` triggers and submit.
43
+ const { supported: voiceSupported, start: startVoice } = useVoiceInput((transcript) => {
44
+ const next = (liveText ? liveText + ' ' : '') + transcript;
45
+ liveText = next;
46
+ const el = promptRef.value as (HTMLElement & { value?: unknown }) | null;
47
+ if (el) el.value = [{ type: 'text', text: next }];
48
+ });
49
+
50
+ function handleVoice() {
51
+ // Mic is Chromium-only; degrade gracefully everywhere else.
52
+ if (!voiceSupported) {
53
+ alert('Voice input needs a Chromium browser.');
54
+ return;
55
+ }
56
+ startVoice();
57
+ }
58
+
59
+ function onValueChange(e: Event) {
60
+ liveText = (e as CustomEvent<{ value: string }>).detail.value;
61
+ }
62
+
63
+ function onSubmit(e: Event) {
64
+ // Clear our own uncontrolled input first — reset the mirror + call the element's
65
+ // clear() method (uncontrolled-safe; fires kai-value-change internally, never a
66
+ // string assignment) — then hand the text up. The parent owns append + streaming.
67
+ liveText = '';
68
+ (promptRef.value as (HTMLElement & { clear?: () => void }) | null)?.clear?.();
69
+ emit('submit', (e as CustomEvent<{ value: string }>).detail.value);
70
+ }
71
+
72
+ function onSuggestionClick(e: Event) {
73
+ emit('suggestion', (e as CustomEvent<{ value: string }>).detail.value);
74
+ }
75
+ </script>
76
+
77
+ <template>
78
+ <div class="composer">
79
+ <kai-prompt-input
80
+ ref="promptRef"
81
+ :theme="theme"
82
+ placeholder="Message the demo…"
83
+ :loading="loading"
84
+ :suggestions.prop="suggestions"
85
+ :triggers.prop="triggers"
86
+ :voice.prop="true"
87
+ @kai-voice="handleVoice"
88
+ @kai-value-change="onValueChange"
89
+ @kai-submit="onSubmit"
90
+ @kai-suggestion-click="onSuggestionClick"
91
+ ></kai-prompt-input>
92
+ <p class="composer-hint">
93
+ Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
94
+ </p>
95
+ </div>
96
+ </template>
@@ -0,0 +1,48 @@
1
+ <script setup lang="ts">
2
+ import type { Theme } from '../types';
3
+ import type { Conversation } from '../chat-data';
4
+
5
+ /**
6
+ * The conversation rail — a thin wrapper over `<kai-conversations>`. The `.sidebar` div
7
+ * owns the shell's right border (kept OFF the element so it follows the shell's
8
+ * light/dark tokens, not the light-mode ones the element pins on itself at `:host`).
9
+ * The rail's `collapsed` is CONTROLLED by the app's collapsed state, so it stays in
10
+ * sync with the parent `<kai-resizable-item collapsed>` and re-expands the list on
11
+ * restore; the internal hamburger still reports the toggle intent up via
12
+ * `@kai-toggle-sidebar`.
13
+ *
14
+ * Array/object props (`groups`, `conversations`) MUST be set as DOM properties, so
15
+ * they use the `.prop` modifier. `activeId` / `collapsed` are scalars.
16
+ */
17
+ defineProps<{
18
+ theme: Theme;
19
+ conversations: Conversation[];
20
+ activeId: string;
21
+ collapsed: boolean;
22
+ }>();
23
+
24
+ const emit = defineEmits<{
25
+ select: [id: string];
26
+ newChat: [];
27
+ toggle: [];
28
+ }>();
29
+
30
+ function onSelect(e: Event) {
31
+ emit('select', (e as CustomEvent<{ id: string }>).detail.id);
32
+ }
33
+ </script>
34
+
35
+ <template>
36
+ <aside class="sidebar">
37
+ <kai-conversations
38
+ :theme="theme"
39
+ :groups.prop="[]"
40
+ :conversations.prop="conversations"
41
+ :active-id="activeId"
42
+ :collapsed.prop="collapsed"
43
+ @kai-conversation-select="onSelect"
44
+ @kai-new-chat="emit('newChat')"
45
+ @kai-toggle-sidebar="emit('toggle')"
46
+ ></kai-conversations>
47
+ </aside>
48
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { Theme } from '../types';
3
+ import MoonIcon from './icons/MoonIcon.vue';
4
+ import SunIcon from './icons/SunIcon.vue';
5
+
6
+ /**
7
+ * Light/dark switch for the top bar. Shows the moon in light mode (tap -> dark)
8
+ * and the sun in dark mode. `<kai-button>` is icon-only, so it carries the
9
+ * accessible label; the glyph is decorative (`aria-hidden`) in the `icon` slot.
10
+ * The native click on the shadow-root button bubbles, so a plain `@click` works.
11
+ */
12
+ defineProps<{ theme: Theme }>();
13
+ defineEmits<{ toggle: [] }>();
14
+ </script>
15
+
16
+ <template>
17
+ <kai-button
18
+ :theme="theme"
19
+ variant="ghost"
20
+ size="icon"
21
+ label="Toggle light/dark theme"
22
+ @click="$emit('toggle')"
23
+ >
24
+ <MoonIcon v-if="theme === 'light'" slot="icon" aria-hidden="true" />
25
+ <SunIcon v-else slot="icon" aria-hidden="true" />
26
+ </kai-button>
27
+ </template>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue';
3
+ import type { ChatMessage } from '@kitn.ai/ui';
4
+ import { partsToText } from '@kitn.ai/ui/state';
5
+ import type { Theme } from '../types';
6
+
7
+ /**
8
+ * The scrolling message list. `<kai-thread>` owns the message rendering, the
9
+ * centered fixed-width column, and stick-to-bottom scroll, so this component just
10
+ * bakes the per-message actions onto the assistant turns and wires the custom
11
+ * `speak` action to the browser's speech synthesis. `copy` (and the feedback
12
+ * votes) are handled inside the element.
13
+ *
14
+ * `messages` is passed to the element as a DOM PROPERTY (`.prop`); a NEW array
15
+ * reference per streaming chunk is what re-renders it. The `withActions` computed
16
+ * derives that fresh array whenever `messages` changes.
17
+ */
18
+ const props = defineProps<{
19
+ theme: Theme;
20
+ messages: ChatMessage[];
21
+ }>();
22
+
23
+ // <kai-thread> reads `actions` off each message; only assistant replies get them.
24
+ const withActions = computed<ChatMessage[]>(() =>
25
+ props.messages.map((m) =>
26
+ m.role === 'assistant'
27
+ ? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
28
+ : m,
29
+ ),
30
+ );
31
+
32
+ function onMessageAction(e: Event) {
33
+ const detail = (e as CustomEvent<{ messageId: string; action: string }>).detail;
34
+ if (detail.action === 'speak') {
35
+ const m = props.messages.find((x) => x.id === detail.messageId);
36
+ if (!m) return;
37
+ window.speechSynthesis.cancel();
38
+ window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
39
+ }
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <kai-thread
45
+ class="thread"
46
+ :theme="theme"
47
+ :messages.prop="withActions"
48
+ @kai-message-action="onMessageAction"
49
+ ></kai-thread>
50
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Moon glyph — shown in light mode (tap the toggle -> dark). moon/sun aren't in
4
+ * the kit's icon set, so the example owns them. Fallthrough attributes land on the
5
+ * root <svg>, so `slot="icon"` and `aria-hidden` pass through and it drops into
6
+ * <kai-button>'s icon slot.
7
+ */
8
+ withDefaults(defineProps<{ size?: number }>(), { size: 20 });
9
+ </script>
10
+
11
+ <template>
12
+ <svg
13
+ :width="size"
14
+ :height="size"
15
+ viewBox="0 0 24 24"
16
+ fill="none"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ >
22
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
23
+ </svg>
24
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Sun glyph — shown in dark mode (tap the toggle -> light). See MoonIcon for why
4
+ * the example owns these icons. Fallthrough attributes land on the root <svg>.
5
+ */
6
+ withDefaults(defineProps<{ size?: number }>(), { size: 20 });
7
+ </script>
8
+
9
+ <template>
10
+ <svg
11
+ :width="size"
12
+ :height="size"
13
+ viewBox="0 0 24 24"
14
+ fill="none"
15
+ stroke="currentColor"
16
+ stroke-width="2"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ >
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
+ </template>
@@ -0,0 +1,3 @@
1
+ export { useChat, type ChatController } from './useChat';
2
+ export { useConversations } from './useConversations';
3
+ export { useVoiceInput } from './useVoiceInput';
@@ -0,0 +1,47 @@
1
+ import { ref, shallowRef, type Ref } from 'vue';
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
+ * Vue 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 `shallowRef` on purpose: every op assigns a BRAND-NEW array
18
+ * reference (`messages.value = updater(prev)`), which is exactly what re-renders
19
+ * `<kai-thread>` — mutating the array in place does not. A shallowRef also keeps
20
+ * the array plain (no deep proxy), so what reaches the element's `messages`
21
+ * property is a clean `ChatMessage[]`.
22
+ */
23
+ export interface ChatController {
24
+ messages: Ref<ChatMessage[]>;
25
+ loading: Ref<boolean>;
26
+ append: (msg: ChatMessage) => void;
27
+ setMessages: (next: ChatMessage[]) => void;
28
+ streamAssistant: (init?: Partial<ChatMessage>) => AssistantStream;
29
+ }
30
+
31
+ export function useChat(initialMessages: ChatMessage[] = []): ChatController {
32
+ const messages = shallowRef<ChatMessage[]>([...initialMessages]);
33
+ const loading = ref(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.value = updater(messages.value); };
38
+
39
+ const append = (msg: ChatMessage) => set((prev) => appendMessage(prev, msg));
40
+ const setMessages = (next: ChatMessage[]) => { messages.value = [...next]; };
41
+ const streamAssistant = (init?: Partial<ChatMessage>): AssistantStream => {
42
+ loading.value = true;
43
+ return onStreamSettled(createAssistantStream(set, init), () => { loading.value = false; });
44
+ };
45
+
46
+ return { messages, loading, append, setMessages, streamAssistant };
47
+ }
@@ -0,0 +1,42 @@
1
+ import { shallowRef, type Ref } from 'vue';
2
+ import { THREADS, newId, type Conversation } from '../chat-data';
3
+ import type { ChatController } from './useChat';
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
8
+ * picked one in. Takes the `useChat` controller so it can read the live messages
9
+ * when stashing and reset them when loading another thread. Mirrors the React
10
+ * `useConversations` hook.
11
+ */
12
+ export interface ConversationsController {
13
+ conversations: Ref<Conversation[]>;
14
+ activeId: Ref<string>;
15
+ selectConversation: (id: string) => void;
16
+ newChat: () => void;
17
+ }
18
+
19
+ export function useConversations(chat: ChatController, initial: Conversation[]): ConversationsController {
20
+ const conversations = shallowRef<Conversation[]>([...initial]);
21
+ const activeId = shallowRef(initial[0].id);
22
+
23
+ const selectConversation = (id: string) => {
24
+ THREADS[activeId.value] = chat.messages.value; // stash the open thread
25
+ activeId.value = id;
26
+ chat.setMessages(THREADS[id] ?? []); // load the picked one
27
+ };
28
+
29
+ const newChat = () => {
30
+ const id = newId();
31
+ THREADS[activeId.value] = chat.messages.value;
32
+ THREADS[id] = [];
33
+ conversations.value = [
34
+ { id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
35
+ ...conversations.value,
36
+ ];
37
+ activeId.value = 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,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,18 @@
1
+ import { createApp } from 'vue';
2
+ import '@kitn.ai/ui/elements'; // registers the kai-* custom elements (async)
3
+ import '@kitn.ai/ui/theme.tokens.css'; // plain --color-* tokens for the shell (NOT the tailwind source)
4
+ import './index.css';
5
+ import App from './App.vue';
6
+
7
+ const root = document.getElementById('app');
8
+ if (!root) throw new Error('Root element not found');
9
+
10
+ // The kai-* elements register asynchronously. Wait until the tags we use are
11
+ // defined before mounting, so Vue's `:prop.prop` bindings land on UPGRADED
12
+ // elements. Raw web-component consumers have no upgrade-race guard (unlike the
13
+ // React wrappers), so a property set before upgrade is lost and static seed data
14
+ // never re-applies. Attributes survive upgrade; array/object properties do not.
15
+ const TAGS = ['kai-resizable', 'kai-resizable-item', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-button'];
16
+ Promise.all(TAGS.map((t) => customElements.whenDefined(t))).then(() => {
17
+ createApp(App).mount(root);
18
+ });
@@ -0,0 +1,4 @@
1
+ // Shared across the example's components. In the React reference this lives in
2
+ // App.tsx (`export type Theme`); a Vue SFC can't re-export a named type as
3
+ // cleanly, so the example keeps it here.
4
+ export type Theme = 'light' | 'dark';
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Vue SFC JSX handling (harmless with no .tsx present) */
18
+ "jsx": "preserve",
19
+ "jsxImportSource": "vue",
20
+
21
+ /* Linting */
22
+ "strict": true,
23
+ "noUnusedLocals": true,
24
+ "noUnusedParameters": true,
25
+ "noFallthroughCasesInSwitch": true,
26
+ "noUncheckedSideEffectImports": true
27
+ },
28
+ "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
29
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "types": ["node"],
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+ "noUncheckedSideEffectImports": true
23
+ },
24
+ "include": ["vite.config.ts"]
25
+ }
@@ -0,0 +1,24 @@
1
+ import { defineConfig } from 'vite';
2
+ import vue from '@vitejs/plugin-vue';
3
+
4
+ // `@kitn.ai/ui` is linked into this example via `workspace:*`, so it resolves
5
+ // through node_modules + the package's `exports` map exactly like a published
6
+ // consumer would — no aliases, no source stubs. Build the kit first (`nx build ui`).
7
+ //
8
+ // The one Vue-specific bit: tell the template compiler that every `kai-*` tag is a
9
+ // native custom element. Without this Vue tries to resolve them as Vue components
10
+ // (warning "unknown custom element"); with it, Vue leaves them alone and passes
11
+ // props/events straight through to the DOM.
12
+ // https://vite.dev/config/
13
+ export default defineConfig({
14
+ plugins: [
15
+ vue({
16
+ template: {
17
+ compilerOptions: {
18
+ isCustomElement: (tag) => tag.startsWith('kai-'),
19
+ },
20
+ },
21
+ }),
22
+ ],
23
+ server: { port: 5174 },
24
+ });