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,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { KaiResizableItemElement } from '@kitn.ai/ui/elements';
|
|
3
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
4
|
+
import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from './chat-data';
|
|
5
|
+
import type { Theme } from './lib/types';
|
|
6
|
+
import { createChat } from './lib/chat.svelte';
|
|
7
|
+
import { createConversations } from './lib/conversations.svelte';
|
|
8
|
+
import Sidebar from './components/Sidebar.svelte';
|
|
9
|
+
import ThreadView from './components/ThreadView.svelte';
|
|
10
|
+
import Composer from './components/Composer.svelte';
|
|
11
|
+
import ThemeToggle from './components/ThemeToggle.svelte';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A mini chat workspace COMPOSED BY HAND from @kitn.ai/ui's individual elements —
|
|
15
|
+
* the Svelte mirror of `examples/react` + `examples/vue`. It shows how the raw
|
|
16
|
+
* `kai-*` web components fit together (vs. dropping in one batteries-included
|
|
17
|
+
* `<kai-chat>`/`<kai-workspace>`):
|
|
18
|
+
*
|
|
19
|
+
* <kai-resizable>/<kai-resizable-item> — the draggable sidebar | main split (the
|
|
20
|
+
* divider is the kit's default `line` hairline)
|
|
21
|
+
* <kai-conversations> — the sidebar list (fed `conversations`, emits select/new)
|
|
22
|
+
* <kai-thread> — the scrolling message list (stick-to-bottom built in)
|
|
23
|
+
* <kai-prompt-input> — the composer at the bottom
|
|
24
|
+
*
|
|
25
|
+
* The pieces are split into `components/` (the UI subcomponents + the example's own
|
|
26
|
+
* moon/sun icons) and `lib/` (`createChat` owns the message array + streaming,
|
|
27
|
+
* `createConversations` the conversation stash, `createVoiceInput` the mic).
|
|
28
|
+
* Everything else is plain Svelte runes. Swap `mockResponse(text)` for a real
|
|
29
|
+
* `fetch` to ship.
|
|
30
|
+
*/
|
|
31
|
+
let theme: Theme = $state('dark');
|
|
32
|
+
let collapsed = $state(false);
|
|
33
|
+
const chat = createChat(THREADS[CONVERSATIONS[0].id] ?? []);
|
|
34
|
+
const conversations = createConversations(chat, CONVERSATIONS);
|
|
35
|
+
|
|
36
|
+
const suggestions: string[] = $derived(chat.messages.length <= 1 ? SUGGESTIONS : []);
|
|
37
|
+
|
|
38
|
+
// The sidebar <kai-resizable-item>'s `collapsed` boolean is a DOM property the
|
|
39
|
+
// parent <kai-resizable> reads (reflected to an attribute the group observes), so
|
|
40
|
+
// drive it imperatively rather than as a bare attribute.
|
|
41
|
+
let sidebarItem: KaiResizableItemElement;
|
|
42
|
+
$effect(() => { sidebarItem.collapsed = collapsed; });
|
|
43
|
+
|
|
44
|
+
async function send(raw: string) {
|
|
45
|
+
const text = raw.trim();
|
|
46
|
+
if (!text) return;
|
|
47
|
+
// The Composer already cleared its own input; here we just append the user
|
|
48
|
+
// message and stream the (mock) assistant reply.
|
|
49
|
+
chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
50
|
+
const stream = chat.streamAssistant();
|
|
51
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that go
|
|
52
|
+
// through the SAME reader a real model's response would, so this preview
|
|
53
|
+
// exercises the real path. To go live, only this one expression changes —
|
|
54
|
+
// `mockResponse(text)` becomes a POST to your route, with
|
|
55
|
+
// toOpenAIMessages(chat.messages) as the body. The line below stays.
|
|
56
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
57
|
+
stream.done();
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<!-- <kai-resizable> owns the sidebar width + the divider. The handle defaults to the
|
|
62
|
+
`line` hairline (transparent at rest, tinting on hover/drag); collapsing the
|
|
63
|
+
sidebar maps to <kai-resizable-item collapsed>. Pass `theme` to the group AND
|
|
64
|
+
every item so slotted chrome inherits the right light/dark tokens. -->
|
|
65
|
+
<div class="app" class:dark={theme === 'dark'}>
|
|
66
|
+
<kai-resizable {theme} orientation="horizontal">
|
|
67
|
+
<kai-resizable-item bind:this={sidebarItem} {theme} size="280px" min="220px" max="420px">
|
|
68
|
+
<Sidebar
|
|
69
|
+
{theme}
|
|
70
|
+
conversations={conversations.conversations}
|
|
71
|
+
activeId={conversations.activeId}
|
|
72
|
+
{collapsed}
|
|
73
|
+
onselect={conversations.selectConversation}
|
|
74
|
+
onnewchat={conversations.newChat}
|
|
75
|
+
ontoggle={() => (collapsed = !collapsed)}
|
|
76
|
+
/>
|
|
77
|
+
</kai-resizable-item>
|
|
78
|
+
|
|
79
|
+
<kai-resizable-item {theme}>
|
|
80
|
+
<main class="main">
|
|
81
|
+
<header class="bar">
|
|
82
|
+
<div class="bar-left">
|
|
83
|
+
{#if collapsed}
|
|
84
|
+
<!-- Comma-separated on purpose: in runes mode Svelte reads everything after the
|
|
85
|
+
first uncommaed code as prose, so the space-separated form suppresses only the
|
|
86
|
+
first rule. Both are false positives over a shadow root that already holds a
|
|
87
|
+
real focusable `<button>`; see the block in components/ThemeToggle.svelte. -->
|
|
88
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
89
|
+
<kai-button
|
|
90
|
+
{theme}
|
|
91
|
+
variant="ghost"
|
|
92
|
+
size="icon"
|
|
93
|
+
icon="panel-left"
|
|
94
|
+
label="Show sidebar"
|
|
95
|
+
onclick={() => (collapsed = false)}
|
|
96
|
+
></kai-button>
|
|
97
|
+
{/if}
|
|
98
|
+
<span class="brand">@kitn.ai/ui · composed chat</span>
|
|
99
|
+
</div>
|
|
100
|
+
<ThemeToggle {theme} ontoggle={() => (theme = theme === 'light' ? 'dark' : 'light')} />
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
<ThreadView {theme} messages={chat.messages} />
|
|
104
|
+
|
|
105
|
+
<Composer
|
|
106
|
+
{theme}
|
|
107
|
+
loading={chat.loading}
|
|
108
|
+
{suggestions}
|
|
109
|
+
triggers={TRIGGERS}
|
|
110
|
+
onsubmit={send}
|
|
111
|
+
onsuggestion={send}
|
|
112
|
+
/>
|
|
113
|
+
</main>
|
|
114
|
+
</kai-resizable-item>
|
|
115
|
+
</kai-resizable>
|
|
116
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
/// <reference types="vite/client" />
|
|
3
|
+
|
|
4
|
+
// The raw `kai-*` elements are DOM custom elements (not Svelte components), so they
|
|
5
|
+
// have no generated prop types. Declare the tags this example uses so the template
|
|
6
|
+
// accepts their scalar attributes (`theme`, `size`, `active-id`, …), their rich
|
|
7
|
+
// props, and their `onkai-*` event handlers without type errors. Rich array/object
|
|
8
|
+
// props and boolean flags are set imperatively as DOM PROPERTIES in each component
|
|
9
|
+
// (see the `bind:this` + `$effect` blocks), which is why a permissive attribute map
|
|
10
|
+
// here is enough.
|
|
11
|
+
declare namespace svelteHTML {
|
|
12
|
+
interface IntrinsicElements {
|
|
13
|
+
'kai-resizable': Record<string, unknown>;
|
|
14
|
+
'kai-resizable-item': Record<string, unknown>;
|
|
15
|
+
'kai-conversations': Record<string, unknown>;
|
|
16
|
+
'kai-thread': Record<string, unknown>;
|
|
17
|
+
'kai-prompt-input': Record<string, unknown>;
|
|
18
|
+
'kai-button': Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Self-contained sample data + the kit's shared mock responder, no backend.
|
|
2
|
+
import type { MessagePart } from '@kitn.ai/ui';
|
|
3
|
+
import { createMockResponder } from '@kitn.ai/ui/state';
|
|
4
|
+
|
|
5
|
+
export interface Conversation {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
scope: { type: 'document' | 'collection' };
|
|
9
|
+
messageCount: number;
|
|
10
|
+
lastMessageAt: string;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Msg {
|
|
14
|
+
id: string;
|
|
15
|
+
role: 'user' | 'assistant';
|
|
16
|
+
parts: MessagePart[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function newId(): string {
|
|
20
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
21
|
+
? crypto.randomUUID()
|
|
22
|
+
: 'id-' + Math.random().toString(36).slice(2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const iso = (daysAgo = 0) => new Date(Date.now() - daysAgo * 86_400_000).toISOString();
|
|
26
|
+
|
|
27
|
+
export const CONVERSATIONS: Conversation[] = [
|
|
28
|
+
{ id: 'c1', title: 'Getting started', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(0), updatedAt: iso(0) },
|
|
29
|
+
{ id: 'c2', title: 'Composing your own chat', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(0), updatedAt: iso(0) },
|
|
30
|
+
{ id: 'c3', title: 'Theming + dark mode', scope: { type: 'collection' }, messageCount: 2, lastMessageAt: iso(1), updatedAt: iso(1) },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export const THREADS: Record<string, Msg[]> = {
|
|
34
|
+
c1: [
|
|
35
|
+
{ id: 'c1u', role: 'user', parts: [{ type: 'text', text: 'What is @kitn.ai/ui?' }] },
|
|
36
|
+
{ id: 'c1a', role: 'assistant', parts: [{ type: 'text', text: '**@kitn.ai/ui** is a set of framework-agnostic web components for AI chat UIs: message threads, streaming, markdown, tool panels, and more. This demo composes its `<kai-conversations>`, `<kai-thread>`, and `<kai-prompt-input>` elements by hand.' }] },
|
|
37
|
+
],
|
|
38
|
+
c2: [
|
|
39
|
+
{ id: 'c2u', role: 'user', parts: [{ type: 'text', text: 'How do I build my own chat instead of dropping in `<kai-chat>`?' }] },
|
|
40
|
+
{ id: 'c2a', role: 'assistant', parts: [{ type: 'text', text: "Feed your `messages` to a `<kai-thread>`, put a `<kai-prompt-input>` below, and wire `kai-submit` to append + stream. That's exactly what this example does. Read `App.svelte`." }] },
|
|
41
|
+
],
|
|
42
|
+
c3: [
|
|
43
|
+
{ id: 'c3u', role: 'user', parts: [{ type: 'text', text: 'How does dark mode work?' }] },
|
|
44
|
+
{ id: 'c3a', role: 'assistant', parts: [{ type: 'text', text: "Each element takes a `theme` prop (`light` / `dark` / `auto`). Drive it from app state. Toggle it with the button top-right. The kit's `--color-*` tokens flip under a `.dark` class for your own surrounding chrome." }] },
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const SUGGESTIONS = ['What is @kitn.ai/ui?', 'How do I install it?', 'Show me some markdown'];
|
|
49
|
+
|
|
50
|
+
// Rich entity triggers for the prompt input: typing `/` opens the skills menu,
|
|
51
|
+
// `@` opens the agents menu. Each selection inserts an atomic pill. Set on
|
|
52
|
+
// <kai-prompt-input> as the `triggers` JS property (a TriggerDef[]).
|
|
53
|
+
export const TRIGGERS = [
|
|
54
|
+
{
|
|
55
|
+
char: '/',
|
|
56
|
+
kind: 'skill',
|
|
57
|
+
items: [
|
|
58
|
+
{ id: 'summarize', label: 'Summarize', description: 'Summarize the thread', promptText: 'Summarize the thread.' },
|
|
59
|
+
{ id: 'translate', label: 'Translate', description: 'Translate to English', promptText: 'Translate to English.' },
|
|
60
|
+
{ id: 'rewrite', label: 'Rewrite', description: 'Rewrite for clarity', promptText: 'Rewrite this for clarity.' },
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
char: '@',
|
|
65
|
+
kind: 'agent',
|
|
66
|
+
items: [
|
|
67
|
+
{ id: 'researcher', label: 'Researcher', description: 'Deep web research', group: 'Agents' },
|
|
68
|
+
{ id: 'coder', label: 'Coder', description: 'Writes and edits code', group: 'Agents' },
|
|
69
|
+
{ id: 'designer', label: 'Designer', description: 'UI and visual design', group: 'Agents' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// The replies the mock cycles through, one per turn. Markdown-rich on purpose:
|
|
75
|
+
// the thread renders markdown, code blocks and blockquotes, so the canned
|
|
76
|
+
// replies exercise that instead of only proving text arrives.
|
|
77
|
+
//
|
|
78
|
+
// The first one says it is a mock in words. That is the WEAKEST of the
|
|
79
|
+
// responder's tells and the only one a real model could imitate, which is
|
|
80
|
+
// exactly why it is not the only one — the rest are on the wire, below.
|
|
81
|
+
const MOCK_REPLIES = [
|
|
82
|
+
"This reply streams token-by-token from the kit's own mock responder. No API key, no backend, no provider was contacted. Replace `mockResponse(text)` with a real model call (Anthropic, OpenAI, your own endpoint) to ship a real app.",
|
|
83
|
+
"Install it:\n\n```bash\nnpm install @kitn.ai/ui\n```\n\nThen register the elements + load the tokens:\n\n```ts\nimport '@kitn.ai/ui/elements';\nimport '@kitn.ai/ui/theme.tokens.css';\n```\n\nNow drop `<kai-thread>` and `<kai-prompt-input>` into your template.",
|
|
84
|
+
"I render **bold**, *italic*, `inline code`, lists, and code blocks:\n\n```ts\nconst stream = chat.streamAssistant();\nawait readOpenAIStream(mockResponse(text), stream);\nstream.done();\n```\n\n> Blockquotes too.",
|
|
85
|
+
"This chat is **composed by hand**: `<kai-conversations>` for the sidebar, a `<kai-thread>` for the messages, and `<kai-prompt-input>` at the bottom, all wired with plain reactive state. Swap `mockResponse(text)` for your own model call and you're shipping.",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The kit's own mock responder, shared with the `kai` MCP scaffolder and
|
|
90
|
+
* `create-kai` so there is ONE implementation of this and not seven.
|
|
91
|
+
*
|
|
92
|
+
* It yields canned SSE frames in the OpenAI chat-completions shape, which
|
|
93
|
+
* `readOpenAIStream` parses exactly as it parses a real provider's. So this
|
|
94
|
+
* no-backend preview runs the kit's REAL streaming path — the SSE reader, the
|
|
95
|
+
* part folding, all of it — rather than a hand-rolled loop that merely
|
|
96
|
+
* resembles it. Going live then changes one expression, not the handler.
|
|
97
|
+
*
|
|
98
|
+
* It also cannot be mistaken for a real turn: the stream opens with a
|
|
99
|
+
* `: kai-mock` SSE comment, every frame carries a `_kai_mock` field, `model`
|
|
100
|
+
* reports as `kai-mock` (no provider serves that, so an echoed mock frame is
|
|
101
|
+
* rejected upstream rather than quietly believed), and usage is all zeros.
|
|
102
|
+
*
|
|
103
|
+
* MODULE scope, not per-send: the responder owns the cursor into the replies
|
|
104
|
+
* above, so rebuilding it each turn would answer with the first one forever.
|
|
105
|
+
*/
|
|
106
|
+
export const mockResponse = createMockResponder({ replies: MOCK_REPLIES });
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { KaiPromptInputElement } from '@kitn.ai/ui/elements';
|
|
3
|
+
import type { Theme } from '../lib/types';
|
|
4
|
+
import { createVoiceInput } from '../lib/voiceInput';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
|
|
8
|
+
* the element ref, a live text mirror, clear-on-submit, and seeding voice.
|
|
9
|
+
*
|
|
10
|
+
* The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
|
|
11
|
+
* caret. We NEVER bind a plain STRING `value`: that flips the element into
|
|
12
|
+
* controlled mode, which re-applies the property and collapses the shadow-DOM
|
|
13
|
+
* selection, disabling the caret-anchored menu. So clear-on-submit goes through
|
|
14
|
+
* the element's `clear()` method, and voice seeds a ComposerDoc (a non-string
|
|
15
|
+
* value is a one-time seed that leaves the input uncontrolled). We keep the live
|
|
16
|
+
* text mirrored in `liveText` (from `kai-value-change`) so voice can append.
|
|
17
|
+
*
|
|
18
|
+
* Rich props (`suggestions`, `triggers`) and boolean flags (`loading`, `voice`)
|
|
19
|
+
* are set imperatively as DOM PROPERTIES (`bind:this` + `$effect`): `voice` in
|
|
20
|
+
* particular MUST be a truthy PROPERTY (`el.voice = true`), not a bare attribute,
|
|
21
|
+
* or the facade reads the mic control as off.
|
|
22
|
+
*/
|
|
23
|
+
let {
|
|
24
|
+
theme,
|
|
25
|
+
loading,
|
|
26
|
+
suggestions,
|
|
27
|
+
triggers,
|
|
28
|
+
onsubmit,
|
|
29
|
+
onsuggestion,
|
|
30
|
+
}: {
|
|
31
|
+
theme: Theme;
|
|
32
|
+
loading: boolean;
|
|
33
|
+
suggestions: string[];
|
|
34
|
+
triggers: unknown[];
|
|
35
|
+
onsubmit: (value: string) => void;
|
|
36
|
+
onsuggestion: (value: string) => void;
|
|
37
|
+
} = $props();
|
|
38
|
+
|
|
39
|
+
// Ref to the underlying <kai-prompt-input> for the imperative pushes (rich props,
|
|
40
|
+
// flags, clear() on submit, ComposerDoc seed from voice); `liveText` mirrors text.
|
|
41
|
+
// The kit ships a typed element interface; intersect the imperative `clear()`
|
|
42
|
+
// method the facade exposes (methods aren't part of the generated prop interface).
|
|
43
|
+
let el: KaiPromptInputElement & { clear?: () => void };
|
|
44
|
+
let liveText = '';
|
|
45
|
+
|
|
46
|
+
$effect(() => { el.suggestions = suggestions; });
|
|
47
|
+
$effect(() => { el.triggers = triggers as KaiPromptInputElement['triggers']; });
|
|
48
|
+
$effect(() => { el.loading = loading; });
|
|
49
|
+
// A truthy PROPERTY, never a bare `voice` attribute. Runs once (no reactive deps).
|
|
50
|
+
$effect(() => { el.voice = true; });
|
|
51
|
+
|
|
52
|
+
// Voice: on a transcript, append it to the current text and seed the (uncontrolled)
|
|
53
|
+
// input as a ComposerDoc. A non-string `value` is a one-time seed into the
|
|
54
|
+
// element's internal state; a plain STRING would instead flip it into controlled
|
|
55
|
+
// mode and re-break the `/`+`@` triggers and submit.
|
|
56
|
+
const { supported: voiceSupported, start: startVoice } = createVoiceInput((transcript) => {
|
|
57
|
+
const next = (liveText ? liveText + ' ' : '') + transcript;
|
|
58
|
+
liveText = next;
|
|
59
|
+
el.value = [{ type: 'text', text: next }];
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function handleVoice() {
|
|
63
|
+
// Mic is Chromium-only; degrade gracefully everywhere else.
|
|
64
|
+
if (!voiceSupported) {
|
|
65
|
+
alert('Voice input needs a Chromium browser.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
startVoice();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function onValueChange(e: Event) {
|
|
72
|
+
liveText = (e as CustomEvent<{ value: string }>).detail.value;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function onSubmit(e: Event) {
|
|
76
|
+
// Clear our own uncontrolled input first — reset the mirror + call the element's
|
|
77
|
+
// clear() method (uncontrolled-safe; fires kai-value-change internally, never a
|
|
78
|
+
// string assignment) — then hand the text up. The parent owns append + streaming.
|
|
79
|
+
liveText = '';
|
|
80
|
+
el.clear?.();
|
|
81
|
+
onsubmit((e as CustomEvent<{ value: string }>).detail.value);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function onSuggestionClick(e: Event) {
|
|
85
|
+
onsuggestion((e as CustomEvent<{ value: string }>).detail.value);
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<div class="composer">
|
|
90
|
+
<kai-prompt-input
|
|
91
|
+
bind:this={el}
|
|
92
|
+
{theme}
|
|
93
|
+
placeholder="Message the demo…"
|
|
94
|
+
onkai-voice={handleVoice}
|
|
95
|
+
onkai-value-change={onValueChange}
|
|
96
|
+
onkai-submit={onSubmit}
|
|
97
|
+
onkai-suggestion-click={onSuggestionClick}
|
|
98
|
+
></kai-prompt-input>
|
|
99
|
+
<p class="composer-hint">
|
|
100
|
+
Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { KaiConversationsElement } from '@kitn.ai/ui/elements';
|
|
3
|
+
import type { Theme } from '../lib/types';
|
|
4
|
+
import type { Conversation } from '../chat-data';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The conversation rail — a thin wrapper over `<kai-conversations>`. The `.sidebar`
|
|
8
|
+
* div owns the shell's right border (kept OFF the element so it follows the shell's
|
|
9
|
+
* light/dark tokens, not the light-mode ones the element pins on itself at `:host`).
|
|
10
|
+
* The rail's `collapsed` is CONTROLLED by the app's collapsed state, so it stays in
|
|
11
|
+
* sync with the parent `<kai-resizable-item collapsed>` and re-expands on restore;
|
|
12
|
+
* the internal hamburger still reports the toggle intent up via
|
|
13
|
+
* `onkai-toggle-sidebar`.
|
|
14
|
+
*
|
|
15
|
+
* Array/object props (`groups`, `conversations`) and the boolean `collapsed` flag
|
|
16
|
+
* are set imperatively as DOM PROPERTIES (`bind:this` + `$effect`) so they land on
|
|
17
|
+
* the upgraded element rather than as attributes. `theme` / `active-id` are scalar
|
|
18
|
+
* attributes.
|
|
19
|
+
*/
|
|
20
|
+
let {
|
|
21
|
+
theme,
|
|
22
|
+
conversations,
|
|
23
|
+
activeId,
|
|
24
|
+
collapsed,
|
|
25
|
+
onselect,
|
|
26
|
+
onnewchat,
|
|
27
|
+
ontoggle,
|
|
28
|
+
}: {
|
|
29
|
+
theme: Theme;
|
|
30
|
+
conversations: Conversation[];
|
|
31
|
+
activeId: string;
|
|
32
|
+
collapsed: boolean;
|
|
33
|
+
onselect: (id: string) => void;
|
|
34
|
+
onnewchat: () => void;
|
|
35
|
+
ontoggle: () => void;
|
|
36
|
+
} = $props();
|
|
37
|
+
|
|
38
|
+
// The kit ships a typed element interface (via `HTMLElementTagNameMap`), so the
|
|
39
|
+
// `bind:this` ref + its rich props are fully typed — no casts needed.
|
|
40
|
+
let el: KaiConversationsElement;
|
|
41
|
+
|
|
42
|
+
// `groups` never changes, so this effect has no reactive deps and runs once.
|
|
43
|
+
$effect(() => { el.groups = []; });
|
|
44
|
+
$effect(() => { el.conversations = conversations; });
|
|
45
|
+
$effect(() => { el.collapsed = collapsed; });
|
|
46
|
+
|
|
47
|
+
function onSelect(e: Event) {
|
|
48
|
+
onselect((e as CustomEvent<{ id: string }>).detail.id);
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<aside class="sidebar">
|
|
53
|
+
<kai-conversations
|
|
54
|
+
bind:this={el}
|
|
55
|
+
{theme}
|
|
56
|
+
active-id={activeId}
|
|
57
|
+
onkai-conversation-select={onSelect}
|
|
58
|
+
onkai-new-chat={onnewchat}
|
|
59
|
+
onkai-toggle-sidebar={ontoggle}
|
|
60
|
+
></kai-conversations>
|
|
61
|
+
</aside>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Theme } from '../lib/types';
|
|
3
|
+
import MoonIcon from './icons/MoonIcon.svelte';
|
|
4
|
+
import SunIcon from './icons/SunIcon.svelte';
|
|
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
|
+
* `<kai-button>` fires the native bubbling `click`, so a plain `onclick` works.
|
|
11
|
+
*/
|
|
12
|
+
let { theme, ontoggle }: { theme: Theme; ontoggle: () => void } = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<!--
|
|
16
|
+
Both suppressed rules are false positives HERE, and the justification is the
|
|
17
|
+
shadow root Svelte's a11y pass cannot see into. It sees an unknown tag with a
|
|
18
|
+
click handler; the button semantics are one level down.
|
|
19
|
+
|
|
20
|
+
Measured in chromium against this starter's own build: the accessibility tree
|
|
21
|
+
holds a single `button` named "Toggle light/dark theme" (named from the
|
|
22
|
+
`aria-label` the kit puts on its inner `<button>` from the `label` prop), and
|
|
23
|
+
the `<kai-button>` host is ignored as "uninteresting" and contributes no node.
|
|
24
|
+
Tab reaches that inner button, and Enter and Space activate it natively. The
|
|
25
|
+
resulting click is composed, so it crosses the shadow boundary and lands on
|
|
26
|
+
this `onclick`. Keyboard operation is real, not asserted.
|
|
27
|
+
|
|
28
|
+
Do NOT satisfy the second warning literally by putting a role on the host.
|
|
29
|
+
Measured the same way, `role="button"` there turns the one node into TWO
|
|
30
|
+
identically named buttons, and the outer one has tabindex -1, so it is a
|
|
31
|
+
control screen readers announce and keyboard users cannot reach. The kit
|
|
32
|
+
scrubs stray host roles for this reason (`liftRoleOffHost`, kai-message).
|
|
33
|
+
|
|
34
|
+
The codes MUST be comma-separated. In runes mode Svelte reads everything after
|
|
35
|
+
the first uncommaed code as prose, so the space-separated form silently
|
|
36
|
+
suppressed only `a11y_click_events_have_key_events` and let the other through.
|
|
37
|
+
-->
|
|
38
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
39
|
+
<kai-button {theme} variant="ghost" size="icon" label="Toggle light/dark theme" onclick={ontoggle}>
|
|
40
|
+
{#if theme === 'light'}
|
|
41
|
+
<MoonIcon slot="icon" aria-hidden="true" />
|
|
42
|
+
{:else}
|
|
43
|
+
<SunIcon slot="icon" aria-hidden="true" />
|
|
44
|
+
{/if}
|
|
45
|
+
</kai-button>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ChatMessage } from '@kitn.ai/ui';
|
|
3
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
4
|
+
import type { KaiThreadElement } from '@kitn.ai/ui/elements';
|
|
5
|
+
import type { Theme } from '../lib/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 set on the element as a DOM PROPERTY (`bind:this` + `$effect`); a
|
|
15
|
+
* NEW array reference per streaming chunk is what re-renders it. The `withActions`
|
|
16
|
+
* `$derived` produces that fresh array whenever `messages` changes.
|
|
17
|
+
*/
|
|
18
|
+
let { theme, messages }: { theme: Theme; messages: ChatMessage[] } = $props();
|
|
19
|
+
|
|
20
|
+
let el: KaiThreadElement;
|
|
21
|
+
|
|
22
|
+
// <kai-thread> reads `actions` off each message; only assistant replies get them.
|
|
23
|
+
const withActions: ChatMessage[] = $derived(
|
|
24
|
+
messages.map((m) =>
|
|
25
|
+
m.role === 'assistant'
|
|
26
|
+
? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
|
|
27
|
+
: m,
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
$effect(() => { el.messages = withActions; });
|
|
32
|
+
|
|
33
|
+
function onMessageAction(e: Event) {
|
|
34
|
+
const detail = (e as CustomEvent<{ messageId: string; action: string }>).detail;
|
|
35
|
+
if (detail.action === 'speak') {
|
|
36
|
+
const m = messages.find((x) => x.id === detail.messageId);
|
|
37
|
+
if (!m) return;
|
|
38
|
+
window.speechSynthesis.cancel();
|
|
39
|
+
window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<kai-thread
|
|
45
|
+
class="thread"
|
|
46
|
+
bind:this={el}
|
|
47
|
+
{theme}
|
|
48
|
+
onkai-message-action={onMessageAction}
|
|
49
|
+
></kai-thread>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script 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. The parent (`<kai-button>`) is a
|
|
5
|
+
* custom element, so extra props (`slot="icon"`, `aria-hidden`) pass through as a
|
|
6
|
+
* prop bag; spreading `...rest` onto the root <svg> lands them on the DOM node so
|
|
7
|
+
* it drops into <kai-button>'s `icon` slot. Mirrors the Vue icon's fallthrough.
|
|
8
|
+
*/
|
|
9
|
+
let { size = 20, ...rest }: { size?: number } & Record<string, unknown> = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
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
|
+
{...rest}
|
|
22
|
+
>
|
|
23
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Sun glyph — shown in dark mode (tap the toggle -> light). See MoonIcon for why
|
|
4
|
+
* the example owns these icons and how the spread lands `slot`/`aria-hidden` on
|
|
5
|
+
* the root <svg>.
|
|
6
|
+
*/
|
|
7
|
+
let { size = 20, ...rest }: { size?: number } & Record<string, unknown> = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
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
|
+
{...rest}
|
|
20
|
+
>
|
|
21
|
+
<circle cx="12" cy="12" r="4" />
|
|
22
|
+
<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" />
|
|
23
|
+
<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" />
|
|
24
|
+
</svg>
|
|
@@ -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
|
+
}
|