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,90 @@
1
+ import { createSignal, For, Show } from 'solid-js';
2
+ import {
3
+ Button,
4
+ PromptInput,
5
+ PromptInputActions,
6
+ PromptInputTextarea,
7
+ PromptSuggestion,
8
+ } from '@kitn.ai/ui/solid';
9
+ import { ArrowUpIcon, PlusIcon } from './icons';
10
+
11
+ interface ComposerProps {
12
+ loading: () => boolean;
13
+ suggestions: () => string[];
14
+ onSubmit: (value: string) => void;
15
+ }
16
+
17
+ /**
18
+ * The bottom composer: the suggestion chips, the text input and the send button.
19
+ *
20
+ * It owns the DRAFT — the text being typed — and nothing else. That is the seam
21
+ * worth noticing: the draft is the composer's business, and the moment it is
22
+ * submitted it stops being a draft and becomes a message, which is `App`'s. So
23
+ * `onSubmit` hands the text up and the input clears itself here; the parent
24
+ * never sees a keystroke.
25
+ *
26
+ * `<PromptInput>` is CONTROLLED here (`value` + `onValueChange`), which is the
27
+ * plain-string case and is fine. The React starter deliberately leaves its
28
+ * `<kai-prompt-input>` uncontrolled instead, because assigning a string value to
29
+ * the ELEMENT re-applies the property and collapses its shadow-DOM selection,
30
+ * which breaks the caret-anchored `/` and `@` trigger menus. That is a web
31
+ * component concern; these are Solid components rendering into the light DOM,
32
+ * with no shadow root and no trigger menus, so the simple form applies.
33
+ */
34
+ export function Composer(props: ComposerProps) {
35
+ const [draft, setDraft] = createSignal('');
36
+
37
+ const submit = () => {
38
+ const text = draft().trim();
39
+ if (!text || props.loading()) return;
40
+ setDraft('');
41
+ props.onSubmit(text);
42
+ };
43
+
44
+ return (
45
+ <div class="shrink-0 bg-background px-3 pb-3 md:px-5 md:pb-5">
46
+ <div class="mx-auto max-w-3xl">
47
+ {/* Suggestions fill the draft rather than sending it, so the user can edit
48
+ before committing. They fall away once the thread is underway. */}
49
+ <Show when={props.suggestions().length > 0}>
50
+ <div class="flex flex-wrap gap-2 pb-3">
51
+ <For each={props.suggestions()}>
52
+ {(text) => (
53
+ <PromptSuggestion onClick={() => setDraft(text)}>{text}</PromptSuggestion>
54
+ )}
55
+ </For>
56
+ </div>
57
+ </Show>
58
+
59
+ <PromptInput value={draft()} onValueChange={setDraft} onSubmit={submit}>
60
+ <div class="flex flex-col">
61
+ <PromptInputTextarea placeholder="Ask about SolidJS…" class="min-h-[44px] pt-3 pl-4" />
62
+ <PromptInputActions class="mt-2 flex w-full items-center justify-between gap-2 px-3 pb-3">
63
+ <div class="flex items-center gap-2">
64
+ {/* Disabled on purpose: attachments are a real feature of the kit
65
+ (<kai-attachments>), and wiring them is beyond a starter. */}
66
+ <Button variant="outline" size="icon-sm" class="rounded-full" disabled>
67
+ <PlusIcon size={16} aria-hidden />
68
+ <span class="sr-only">Attach a file</span>
69
+ </Button>
70
+ </div>
71
+ <Button
72
+ size="icon-sm"
73
+ class="rounded-full"
74
+ disabled={!draft().trim() || props.loading()}
75
+ onClick={submit}
76
+ >
77
+ <ArrowUpIcon size={16} aria-hidden />
78
+ <span class="sr-only">Send message</span>
79
+ </Button>
80
+ </PromptInputActions>
81
+ </div>
82
+ </PromptInput>
83
+
84
+ <p class="mt-2 text-center text-xs text-muted-foreground">
85
+ Replies come from the kit's mock responder — no model was contacted.
86
+ </p>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,36 @@
1
+ import { ConversationList } from '@kitn.ai/ui/solid';
2
+ import type { ConversationGroup, ConversationSummary } from '@kitn.ai/ui/solid';
3
+
4
+ interface SidebarProps {
5
+ groups: ConversationGroup[];
6
+ conversations: () => ConversationSummary[];
7
+ activeId: () => string;
8
+ onSelect: (id: string) => void;
9
+ onNewChat: () => void;
10
+ onToggleSidebar: () => void;
11
+ }
12
+
13
+ /**
14
+ * The conversation rail — a thin wrapper over `<ConversationList>`, which owns
15
+ * the grouping, the search box, the active highlight and the "New chat" button.
16
+ *
17
+ * The `<aside>` around it carries the surface: `--color-muted` sits one step off
18
+ * `--color-background` in both themes, and a 1px right border marks the
19
+ * sidebar | main division at rest, since `<ResizableHandle>` only tints on
20
+ * hover/drag. Kept on the wrapper rather than on the element so it follows the
21
+ * shell's light/dark tokens.
22
+ */
23
+ export function Sidebar(props: SidebarProps) {
24
+ return (
25
+ <aside class="h-full min-h-0 overflow-hidden border-r border-border bg-muted">
26
+ <ConversationList
27
+ groups={props.groups}
28
+ conversations={props.conversations()}
29
+ activeId={props.activeId()}
30
+ onSelect={props.onSelect}
31
+ onNewChat={props.onNewChat}
32
+ onToggleSidebar={props.onToggleSidebar}
33
+ />
34
+ </aside>
35
+ );
36
+ }
@@ -0,0 +1,25 @@
1
+ import { Show } from 'solid-js';
2
+ import { Button } from '@kitn.ai/ui/solid';
3
+ import { MoonIcon, SunIcon } from './icons';
4
+ import type { Theme } from '../lib/types';
5
+
6
+ interface ThemeToggleProps {
7
+ theme: () => Theme;
8
+ onToggle: () => void;
9
+ }
10
+
11
+ /**
12
+ * Light/dark switch for the top bar. Shows the moon in light mode (tap → dark)
13
+ * and the sun in dark mode. The button is icon-only, so the glyph is decorative
14
+ * (`aria-hidden`) and the accessible name comes from the `sr-only` span.
15
+ */
16
+ export function ThemeToggle(props: ThemeToggleProps) {
17
+ return (
18
+ <Button variant="ghost" size="icon-sm" onClick={props.onToggle}>
19
+ <Show when={props.theme() === 'light'} fallback={<SunIcon size={18} aria-hidden />}>
20
+ <MoonIcon size={18} aria-hidden />
21
+ </Show>
22
+ <span class="sr-only">Toggle light/dark theme</span>
23
+ </Button>
24
+ );
25
+ }
@@ -0,0 +1,122 @@
1
+ import { createMemo, createSignal, For, Show } from 'solid-js';
2
+ import {
3
+ ChatContainer,
4
+ ChatContainerContent,
5
+ ChatContainerScrollAnchor,
6
+ Message,
7
+ MessageBody,
8
+ ScrollButton,
9
+ } from '@kitn.ai/ui/solid';
10
+ import type { ChatMessage, FeedbackVote } from '@kitn.ai/ui/solid';
11
+ import { partsToText } from '@kitn.ai/ui/state';
12
+ import { ASSISTANT_ACTIONS } from '../chat-data';
13
+
14
+ interface ThreadViewProps {
15
+ messages: () => ChatMessage[];
16
+ onRegenerate: (messageId: string) => void;
17
+ }
18
+
19
+ /**
20
+ * The scrolling message list.
21
+ *
22
+ * This is the component that makes the Solid starter different from the other
23
+ * five: React/Vue/Svelte/Angular/vanilla drop in `<kai-thread>`, one tag that
24
+ * owns the whole list. Solid renders the components DIRECTLY, so the list is
25
+ * spelled out — `<ChatContainer>` for the scroll box and stick-to-bottom,
26
+ * `<Message>` per turn, `<MessageBody>` to walk that turn's ordered `parts` and
27
+ * render each kind (text as markdown, reasoning as a collapsible block, a tool
28
+ * call as a panel, cards through the card registry).
29
+ *
30
+ * It owns the action bar's transient state — the vote and the "copied" flag —
31
+ * because `<MessageBody>` is prop-driven and holds no signals of its own. That
32
+ * is view state, so it lives with the view. `regenerate` is NOT: it re-runs the
33
+ * gateway, so it goes up to `App`.
34
+ */
35
+ export function ThreadView(props: ThreadViewProps) {
36
+ const [votes, setVotes] = createSignal<Record<string, FeedbackVote | undefined>>({});
37
+ const [copiedId, setCopiedId] = createSignal<string | null>(null);
38
+
39
+ // The list's KEYS, not the messages. `<For>` is reference-keyed, and every
40
+ // delta rebuilds the streaming message as a new object — so keying on the
41
+ // messages themselves makes each chunk look like a brand-new list and rebuilds
42
+ // the whole row. Diffing ids (plain strings) instead means a delta produces an
43
+ // identical key list, nothing moves, and the row survives the update.
44
+ //
45
+ // Not `<Index>`: position-keying leaves a row's local state (an open tool or
46
+ // reasoning panel) with the SLOT rather than the message, so prepending older
47
+ // turns would shift every open disclosure onto the wrong message. Position is
48
+ // the right key INSIDE a message, which is what `<MessageBody>` does.
49
+ const messageKeys = createMemo(() => props.messages().map((m) => m.id));
50
+
51
+ /** One handler for the whole action bar — `id` is the built-in action name. */
52
+ const handleAction = (msg: ChatMessage, id: string) => {
53
+ if (id === 'copy') {
54
+ void navigator.clipboard?.writeText(partsToText(msg.parts));
55
+ setCopiedId(msg.id);
56
+ setTimeout(() => setCopiedId((c) => (c === msg.id ? null : c)), 2000);
57
+ return;
58
+ }
59
+ if (id === 'like' || id === 'dislike') {
60
+ setVotes((prev) => ({ ...prev, [msg.id]: prev[msg.id] === id ? undefined : id }));
61
+ return;
62
+ }
63
+ if (id === 'regenerate') props.onRegenerate(msg.id);
64
+ };
65
+
66
+ return (
67
+ <div class="relative flex-1 overflow-y-auto">
68
+ <ChatContainer class="h-full">
69
+ <ChatContainerContent class="space-y-0 px-5 pt-4 pb-12">
70
+ <For each={messageKeys()}>
71
+ {(_id, i) => (
72
+ // The row reads its message through <For>'s index accessor, never
73
+ // through a captured value: it outlives the delta that replaced its
74
+ // object, so every read has to go through msg() for the new content
75
+ // to land. <Show> supplies the non-null accessor and covers the
76
+ // frame where a removal has shortened the array.
77
+ <Show when={props.messages()[i()]}>
78
+ {(msg) => (
79
+ <Message
80
+ // `role` is the SPEAKER, not an ARIA role — <Message> turns it
81
+ // into role="article" plus an aria-label naming the speaker,
82
+ // and a data-role hook for styling. Omit it and the row is an
83
+ // unlabelled <div> that tells a screen reader nothing about
84
+ // who is talking. That is #176: every framework reading
85
+ // `m.role` for its alignment classes and then not passing it
86
+ // on. The alignment below reads the same value — if you are
87
+ // deriving CSS from a semantic field, the semantics have to
88
+ // reach the DOM too.
89
+ role={msg().role}
90
+ class={`mx-auto flex w-full max-w-3xl flex-col gap-2 px-6 ${
91
+ msg().role === 'user' ? 'items-end' : 'items-start'
92
+ }`}
93
+ >
94
+ <div class="group flex w-full flex-col gap-0">
95
+ <MessageBody
96
+ parts={msg().parts}
97
+ isUser={msg().role === 'user'}
98
+ markdown={msg().role !== 'user'}
99
+ actions={msg().role === 'user' ? undefined : ASSISTANT_ACTIONS}
100
+ actionsReveal="hover"
101
+ activeFeedback={votes()[msg().id]}
102
+ copied={copiedId() === msg().id}
103
+ onAction={(id) => handleAction(msg(), id)}
104
+ />
105
+ </div>
106
+ </Message>
107
+ )}
108
+ </Show>
109
+ )}
110
+ </For>
111
+
112
+ <ChatContainerScrollAnchor />
113
+ </ChatContainerContent>
114
+
115
+ {/* Scroll-to-bottom button */}
116
+ <div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
117
+ <ScrollButton class="shadow-sm" />
118
+ </div>
119
+ </ChatContainer>
120
+ </div>
121
+ );
122
+ }
@@ -0,0 +1,84 @@
1
+ import { splitProps, type JSX } from 'solid-js';
2
+
3
+ /**
4
+ * The four glyphs this example needs, hand-rolled.
5
+ *
6
+ * WHY NOT AN ICON PACKAGE. This starter used to pull `lucide-solid` in for two
7
+ * of them. Four inline `<path>`s are smaller than the dependency, and they keep
8
+ * the starter's runtime dependencies to exactly `solid-js` + the kit — which is
9
+ * the more honest demonstration, since a reader can then tell which of the
10
+ * things on screen come from `@kitn.ai/ui` and which are their own. Mirrors
11
+ * `examples/starters/react/src/components/icons/`, where moon/sun are owned by
12
+ * the example for the same reason: they are not in the kit's `<Icon>` set.
13
+ *
14
+ * Props spread onto the `<svg>`, so `class` and `aria-hidden` pass through.
15
+ * `splitProps` rather than destructuring: destructuring a Solid props object
16
+ * reads every field once, eagerly, and severs reactivity — these are static
17
+ * today, but the habit is the point in a file people copy.
18
+ */
19
+ export type IconProps = { size?: number } & JSX.SvgSVGAttributes<SVGSVGElement>;
20
+
21
+ function Glyph(props: IconProps & { children: JSX.Element }) {
22
+ const [local, rest] = splitProps(props, ['size', 'children']);
23
+ return (
24
+ <svg
25
+ viewBox="0 0 24 24"
26
+ width={local.size ?? 20}
27
+ height={local.size ?? 20}
28
+ fill="none"
29
+ stroke="currentColor"
30
+ stroke-width={2}
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"
33
+ {...rest}
34
+ >
35
+ {local.children}
36
+ </svg>
37
+ );
38
+ }
39
+
40
+ /** Moon — shown in light mode (tap the toggle → dark). */
41
+ export function MoonIcon(props: IconProps) {
42
+ return (
43
+ <Glyph {...props}>
44
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
45
+ </Glyph>
46
+ );
47
+ }
48
+
49
+ /** Sun — shown in dark mode (tap the toggle → light). */
50
+ export function SunIcon(props: IconProps) {
51
+ return (
52
+ <Glyph {...props}>
53
+ <circle cx="12" cy="12" r="4" />
54
+ <path d="M12 2v2" />
55
+ <path d="M12 20v2" />
56
+ <path d="m4.93 4.93 1.41 1.41" />
57
+ <path d="m17.66 17.66 1.41 1.41" />
58
+ <path d="M2 12h2" />
59
+ <path d="M20 12h2" />
60
+ <path d="m6.34 17.66-1.41 1.41" />
61
+ <path d="m19.07 4.93-1.41 1.41" />
62
+ </Glyph>
63
+ );
64
+ }
65
+
66
+ /** Submit arrow, on the composer's send button. */
67
+ export function ArrowUpIcon(props: IconProps) {
68
+ return (
69
+ <Glyph {...props}>
70
+ <path d="M12 19V5" />
71
+ <path d="m5 12 7-7 7 7" />
72
+ </Glyph>
73
+ );
74
+ }
75
+
76
+ /** Attach — the disabled affordance on the composer, left of centre. */
77
+ export function PlusIcon(props: IconProps) {
78
+ return (
79
+ <Glyph {...props}>
80
+ <path d="M5 12h14" />
81
+ <path d="M12 5v14" />
82
+ </Glyph>
83
+ );
84
+ }
@@ -0,0 +1,5 @@
1
+ import { render } from "solid-js/web";
2
+ import "./styles.css";
3
+ import App from "./App";
4
+
5
+ render(() => <App />, document.getElementById("root")!);
@@ -0,0 +1,64 @@
1
+ import { createSignal, type Accessor } from 'solid-js';
2
+ import type { ChatMessage } from '@kitn.ai/ui/solid';
3
+ import {
4
+ appendMessage,
5
+ createAssistantStream,
6
+ onStreamSettled,
7
+ type AssistantStream,
8
+ type SetMessages,
9
+ } from '@kitn.ai/ui/state';
10
+
11
+ /**
12
+ * Solid 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 and the
14
+ * Svelte starter's `createChat` are built on. It owns the message array + the
15
+ * `loading` flag and exposes ergonomic ops.
16
+ *
17
+ * WHY A `createSignal` AND NOT A STORE. A store's whole selling point is
18
+ * fine-grained mutation of nested fields, and that is the one thing the state
19
+ * core must not do: `createAssistantStream` drives everything through a
20
+ * functional-updater setter that returns a BRAND-NEW array with a brand-new
21
+ * message object inside it, because a fresh reference is what the thread reads
22
+ * as "this changed". A store would either fight that (deep-diffing a whole array
23
+ * we already know is new) or invite someone to reach in and mutate `parts` in
24
+ * place, which does not re-render. Signals model "replace the value" exactly,
25
+ * so the seam between the kit's core and Solid is one line — `set` below.
26
+ *
27
+ * `messages` and `loading` are returned as ACCESSORS, not values. Reading them
28
+ * is what subscribes a component, so handing back `messages()` here would
29
+ * snapshot the array once at call time and never update. Callers write
30
+ * `chat.messages()`.
31
+ */
32
+ export interface ChatController {
33
+ messages: Accessor<ChatMessage[]>;
34
+ loading: Accessor<boolean>;
35
+ append: (msg: ChatMessage) => void;
36
+ setMessages: (next: ChatMessage[]) => void;
37
+ streamAssistant: (init?: Partial<ChatMessage>) => AssistantStream;
38
+ }
39
+
40
+ export function createChat(initialMessages: ChatMessage[] = []): ChatController {
41
+ const [messages, setMessagesSignal] = createSignal<ChatMessage[]>([...initialMessages]);
42
+ const [loading, setLoading] = createSignal(false);
43
+
44
+ // The one universal contract the state core drives: a functional-updater
45
+ // setter (React setState shape). Solid's setter already takes an updater, so
46
+ // this is a straight pass-through — the arrow is kept rather than writing
47
+ // `setMessagesSignal` bare because a signal setter's updater overload is only
48
+ // selected for a FUNCTION argument, and being explicit about which overload we
49
+ // mean costs nothing and reads unambiguously.
50
+ const set: SetMessages = (updater) => setMessagesSignal((prev) => updater(prev));
51
+
52
+ return {
53
+ messages,
54
+ loading,
55
+ append: (msg) => set((prev) => appendMessage(prev, msg)),
56
+ // A fresh array, so swapping conversations is a reference change like every
57
+ // other update rather than a special case the thread has to detect.
58
+ setMessages: (next) => setMessagesSignal([...next]),
59
+ streamAssistant: (init) => {
60
+ setLoading(true);
61
+ return onStreamSettled(createAssistantStream(set, init), () => setLoading(false));
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,54 @@
1
+ import { createSignal, type Accessor } from 'solid-js';
2
+ import type { ConversationSummary } from '@kitn.ai/ui/solid';
3
+ import { THREADS, newId } from '../chat-data';
4
+ import type { ChatController } from './chat';
5
+
6
+ /**
7
+ * Owns the conversation list + which one is active, plus the in-memory `THREADS`
8
+ * stash: on select/new we swap the OPEN thread out to `THREADS` and swap the
9
+ * picked one in. Takes the `createChat` controller so it can read the live
10
+ * messages when stashing and reset them when loading another thread. Mirrors the
11
+ * React/Vue `useConversations` and the Svelte `createConversations`.
12
+ */
13
+ export interface ConversationsController {
14
+ conversations: Accessor<ConversationSummary[]>;
15
+ activeId: Accessor<string>;
16
+ selectConversation: (id: string) => void;
17
+ newChat: () => void;
18
+ }
19
+
20
+ export function createConversations(
21
+ chat: ChatController,
22
+ initial: ConversationSummary[],
23
+ ): ConversationsController {
24
+ const [conversations, setConversations] = createSignal<ConversationSummary[]>([...initial]);
25
+ const [activeId, setActiveId] = createSignal(initial[0].id);
26
+
27
+ const selectConversation = (id: string) => {
28
+ THREADS[activeId()] = chat.messages(); // stash the open thread
29
+ setActiveId(id);
30
+ chat.setMessages(THREADS[id] ?? []); // load the picked one
31
+ };
32
+
33
+ const newChat = () => {
34
+ const id = newId();
35
+ THREADS[activeId()] = chat.messages();
36
+ THREADS[id] = [];
37
+ setConversations((prev) => [
38
+ {
39
+ id,
40
+ title: 'New conversation',
41
+ groupId: 'today',
42
+ scope: { type: 'document' },
43
+ messageCount: 0,
44
+ lastMessageAt: new Date().toISOString(),
45
+ updatedAt: new Date().toISOString(),
46
+ },
47
+ ...prev,
48
+ ]);
49
+ setActiveId(id);
50
+ chat.setMessages([]);
51
+ };
52
+
53
+ return { conversations, activeId, selectConversation, newChat };
54
+ }
@@ -0,0 +1,2 @@
1
+ // Shared across the example's components. Mirrors `examples/starters/svelte/src/lib/types.ts`.
2
+ export type Theme = 'light' | 'dark';
@@ -0,0 +1,11 @@
1
+ @import "tailwindcss";
2
+ @import "@kitn.ai/ui/theme.css";
3
+ @source "../node_modules/@kitn.ai/ui";
4
+
5
+ html,
6
+ body,
7
+ #root {
8
+ height: 100%;
9
+ margin: 0;
10
+ padding: 0;
11
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ESNext", "DOM"],
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "preserve",
14
+ "jsxImportSource": "solid-js",
15
+ "strict": true
16
+ },
17
+ "include": ["src"]
18
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "vite";
2
+ import solidPlugin from "vite-plugin-solid";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+
5
+ export default defineConfig({
6
+ plugins: [
7
+ solidPlugin(),
8
+ tailwindcss(),
9
+ ],
10
+ });
@@ -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 Svelte example</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ html, body, #app { height: 100%; }
10
+ </style>
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ <script type="module" src="/src/main.ts"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@kitn.ai/ui-example-svelte",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "svelte-check --tsconfig ./tsconfig.app.json --fail-on-warnings && vite build",
9
+ "preview": "vite preview",
10
+ "typecheck": "svelte-check --tsconfig ./tsconfig.app.json --fail-on-warnings"
11
+ },
12
+ "dependencies": {
13
+ "@kitn.ai/ui": "workspace:*",
14
+ "svelte": "^5.0.0"
15
+ },
16
+ "devDependencies": {
17
+ "@sveltejs/vite-plugin-svelte": "^5.1.1",
18
+ "@types/node": "^22.19.21",
19
+ "svelte-check": "^4.0.0",
20
+ "typescript": "~5.7.2",
21
+ "vite": "^6.0.0"
22
+ }
23
+ }