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.
- package/README.md +311 -0
- package/dist/index.js +4433 -0
- package/dist/templates/angular/_gitignore +7 -0
- package/dist/templates/angular/angular.json +53 -0
- package/dist/templates/angular/package.json +30 -0
- package/dist/templates/angular/src/app/app.config.ts +11 -0
- package/dist/templates/angular/src/app/app.css +36 -0
- package/dist/templates/angular/src/app/app.html +51 -0
- package/dist/templates/angular/src/app/app.ts +78 -0
- package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
- package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
- package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
- package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
- package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
- package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
- package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
- package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
- package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
- package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
- package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
- package/dist/templates/angular/src/app/types.ts +2 -0
- package/dist/templates/angular/src/chat-data.ts +106 -0
- package/dist/templates/angular/src/index.html +16 -0
- package/dist/templates/angular/src/main.ts +16 -0
- package/dist/templates/angular/src/styles.css +10 -0
- package/dist/templates/angular/tsconfig.app.json +9 -0
- package/dist/templates/angular/tsconfig.json +22 -0
- package/dist/templates/nextjs/_gitignore +11 -0
- package/dist/templates/nextjs/app/chat-data.ts +125 -0
- package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
- package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
- package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
- package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
- package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
- package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
- package/dist/templates/nextjs/app/globals.css +153 -0
- package/dist/templates/nextjs/app/hooks/index.ts +1 -0
- package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
- package/dist/templates/nextjs/app/layout.tsx +50 -0
- package/dist/templates/nextjs/app/page.tsx +17 -0
- package/dist/templates/nextjs/app/theme.ts +10 -0
- package/dist/templates/nextjs/app/workspace.tsx +148 -0
- package/dist/templates/nextjs/next.config.mjs +22 -0
- package/dist/templates/nextjs/package.json +23 -0
- package/dist/templates/nextjs/postcss.config.mjs +16 -0
- package/dist/templates/nextjs/tsconfig.json +23 -0
- package/dist/templates/react/_gitignore +6 -0
- package/dist/templates/react/index.html +16 -0
- package/dist/templates/react/package.json +25 -0
- package/dist/templates/react/src/App.tsx +108 -0
- package/dist/templates/react/src/chat-data.ts +106 -0
- package/dist/templates/react/src/components/Composer.tsx +87 -0
- package/dist/templates/react/src/components/Sidebar.tsx +38 -0
- package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
- package/dist/templates/react/src/components/ThreadView.tsx +40 -0
- package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/react/src/components/icons/index.ts +2 -0
- package/dist/templates/react/src/hooks/index.ts +1 -0
- package/dist/templates/react/src/hooks/useConversations.ts +37 -0
- package/dist/templates/react/src/index.css +111 -0
- package/dist/templates/react/src/main.tsx +15 -0
- package/dist/templates/react/src/vite-env.d.ts +1 -0
- package/dist/templates/react/tsconfig.app.json +28 -0
- package/dist/templates/react/tsconfig.json +7 -0
- package/dist/templates/react/tsconfig.node.json +25 -0
- package/dist/templates/react/vite.config.ts +11 -0
- package/dist/templates/solid/_gitignore +6 -0
- package/dist/templates/solid/index.html +13 -0
- package/dist/templates/solid/package.json +23 -0
- package/dist/templates/solid/src/App.tsx +151 -0
- package/dist/templates/solid/src/chat-data.ts +234 -0
- package/dist/templates/solid/src/components/Composer.tsx +90 -0
- package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
- package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
- package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
- package/dist/templates/solid/src/components/icons/index.tsx +84 -0
- package/dist/templates/solid/src/index.tsx +5 -0
- package/dist/templates/solid/src/lib/chat.ts +64 -0
- package/dist/templates/solid/src/lib/conversations.ts +54 -0
- package/dist/templates/solid/src/lib/types.ts +2 -0
- package/dist/templates/solid/src/styles.css +11 -0
- package/dist/templates/solid/tsconfig.json +18 -0
- package/dist/templates/solid/vite.config.ts +10 -0
- package/dist/templates/svelte/_gitignore +6 -0
- package/dist/templates/svelte/index.html +16 -0
- package/dist/templates/svelte/package.json +23 -0
- package/dist/templates/svelte/src/App.svelte +116 -0
- package/dist/templates/svelte/src/app.d.ts +20 -0
- package/dist/templates/svelte/src/chat-data.ts +106 -0
- package/dist/templates/svelte/src/components/Composer.svelte +102 -0
- package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
- package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
- package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
- package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
- package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
- package/dist/templates/svelte/src/index.css +111 -0
- package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
- package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
- package/dist/templates/svelte/src/lib/types.ts +2 -0
- package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
- package/dist/templates/svelte/src/main.ts +18 -0
- package/dist/templates/svelte/svelte.config.js +8 -0
- package/dist/templates/svelte/tsconfig.app.json +28 -0
- package/dist/templates/svelte/tsconfig.json +7 -0
- package/dist/templates/svelte/tsconfig.node.json +25 -0
- package/dist/templates/svelte/vite.config.ts +15 -0
- package/dist/templates/tanstack-start/_gitignore +14 -0
- package/dist/templates/tanstack-start/package.json +28 -0
- package/dist/templates/tanstack-start/serve.mjs +129 -0
- package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
- package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
- package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
- package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
- package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
- package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
- package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
- package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
- package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
- package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
- package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
- package/dist/templates/tanstack-start/src/router.tsx +16 -0
- package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
- package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
- package/dist/templates/tanstack-start/src/styles.css +153 -0
- package/dist/templates/tanstack-start/src/theme.ts +10 -0
- package/dist/templates/tanstack-start/tsconfig.json +16 -0
- package/dist/templates/tanstack-start/vite.config.ts +28 -0
- package/dist/templates/vanilla/_gitignore +6 -0
- package/dist/templates/vanilla/index.html +16 -0
- package/dist/templates/vanilla/package.json +19 -0
- package/dist/templates/vanilla/src/chat-data.ts +106 -0
- package/dist/templates/vanilla/src/index.css +111 -0
- package/dist/templates/vanilla/src/main.ts +53 -0
- package/dist/templates/vanilla/src/state.ts +119 -0
- package/dist/templates/vanilla/src/view.ts +222 -0
- package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
- package/dist/templates/vanilla/src/voice-input.ts +55 -0
- package/dist/templates/vanilla/tsconfig.json +24 -0
- package/dist/templates/vanilla/vite.config.ts +14 -0
- package/dist/templates/vue/_gitignore +6 -0
- package/dist/templates/vue/index.html +16 -0
- package/dist/templates/vue/package.json +23 -0
- package/dist/templates/vue/src/App.vue +104 -0
- package/dist/templates/vue/src/chat-data.ts +106 -0
- package/dist/templates/vue/src/components/Composer.vue +96 -0
- package/dist/templates/vue/src/components/Sidebar.vue +48 -0
- package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
- package/dist/templates/vue/src/components/ThreadView.vue +50 -0
- package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
- package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
- package/dist/templates/vue/src/composables/index.ts +3 -0
- package/dist/templates/vue/src/composables/useChat.ts +47 -0
- package/dist/templates/vue/src/composables/useConversations.ts +42 -0
- package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
- package/dist/templates/vue/src/index.css +111 -0
- package/dist/templates/vue/src/main.ts +18 -0
- package/dist/templates/vue/src/types.ts +4 -0
- package/dist/templates/vue/src/vite-env.d.ts +1 -0
- package/dist/templates/vue/tsconfig.app.json +29 -0
- package/dist/templates/vue/tsconfig.json +7 -0
- package/dist/templates/vue/tsconfig.node.json +25 -0
- package/dist/templates/vue/vite.config.ts +24 -0
- 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,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,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,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
|
+
}
|