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,16 @@
|
|
|
1
|
+
// NO PLUGINS, ON PURPOSE — and the reason is not "our CSS is plain".
|
|
2
|
+
//
|
|
3
|
+
// This config used to say the app imports "@kitn.ai/ui/theme.css — just custom
|
|
4
|
+
// properties", and that was wrong in a way that built green: `theme.css` is
|
|
5
|
+
// Tailwind v4 SOURCE, its light tokens live in an `@theme { … }` at-rule, and with
|
|
6
|
+
// no Tailwind in this pipeline the browser discarded the whole block. The app now
|
|
7
|
+
// imports the pre-compiled `@kitn.ai/ui/theme.tokens.css` instead (see the note in
|
|
8
|
+
// `app/layout.tsx`), which is real CSS and needs no plugin.
|
|
9
|
+
//
|
|
10
|
+
// The empty config still has a job: without it Next walks up the tree and picks up
|
|
11
|
+
// the monorepo root's Tailwind PostCSS config, which is not installed here. A
|
|
12
|
+
// standalone app outside this repo would not need the file at all — and an app
|
|
13
|
+
// that DOES add Tailwind should add the plugin here and switch the import back to
|
|
14
|
+
// `theme.css`, which is then compiled rather than discarded.
|
|
15
|
+
const config = { plugins: {} };
|
|
16
|
+
export default config;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": {
|
|
18
|
+
"@/*": ["./*"]
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
|
@@ -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 React example</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
html, body, #root { height: 100%; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitn.ai/ui-example-react",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@kitn.ai/ui": "workspace:*",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.19.21",
|
|
19
|
+
"@types/react": "^19.0.0",
|
|
20
|
+
"@types/react-dom": "^19.0.0",
|
|
21
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
22
|
+
"typescript": "~5.7.2",
|
|
23
|
+
"vite": "^6.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { Button, Resizable, ResizableItem, useKaiChat } from '@kitn.ai/ui/react';
|
|
3
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
4
|
+
import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from './chat-data';
|
|
5
|
+
import { Sidebar } from './components/Sidebar';
|
|
6
|
+
import { ThreadView } from './components/ThreadView';
|
|
7
|
+
import { Composer } from './components/Composer';
|
|
8
|
+
import { ThemeToggle } from './components/ThemeToggle';
|
|
9
|
+
import { useConversations } from './hooks';
|
|
10
|
+
|
|
11
|
+
export type Theme = 'light' | 'dark';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A mini chat **workspace composed by hand** from @kitn.ai/ui's individual
|
|
15
|
+
* elements — to show how the pieces fit together (vs. dropping in one
|
|
16
|
+
* batteries-included <kai-chat>/<kai-workspace> tag):
|
|
17
|
+
*
|
|
18
|
+
* <Resizable>/<ResizableItem> — the draggable sidebar | main split (the divider
|
|
19
|
+
* is the kit's default `line` hairline)
|
|
20
|
+
* <Conversations> — the sidebar list (fed `conversations`, emits select/new)
|
|
21
|
+
* <Thread> — the scrolling message list (kai-thread; stick-to-bottom built in)
|
|
22
|
+
* <PromptInput> — the composer at the bottom
|
|
23
|
+
*
|
|
24
|
+
* The pieces are split into `components/` (the UI subcomponents + the example's own
|
|
25
|
+
* moon/sun icons) and `hooks/` (the
|
|
26
|
+
* conversation stash; voice input + the thread scroll now come from the kit), so the
|
|
27
|
+
* structure reads top-down. `useKaiChat` owns the
|
|
28
|
+
* message array + streaming; everything else is plain React state. Swap
|
|
29
|
+
* `mockResponse(text)` for a real `fetch` to ship a real app.
|
|
30
|
+
*/
|
|
31
|
+
export default function App() {
|
|
32
|
+
const [theme, setTheme] = useState<Theme>('dark');
|
|
33
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
34
|
+
const chat = useKaiChat({ initialMessages: THREADS[CONVERSATIONS[0].id] ?? [] });
|
|
35
|
+
const { conversations, activeId, selectConversation, newChat } = useConversations(chat, CONVERSATIONS);
|
|
36
|
+
|
|
37
|
+
const send = useCallback(
|
|
38
|
+
async (raw: string) => {
|
|
39
|
+
const text = raw.trim();
|
|
40
|
+
if (!text) return;
|
|
41
|
+
// The Composer already cleared its own input; here we just append the user
|
|
42
|
+
// message and stream the (mock) assistant reply.
|
|
43
|
+
chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
44
|
+
const stream = chat.streamAssistant();
|
|
45
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that
|
|
46
|
+
// go through the SAME reader a real model's response would, so this
|
|
47
|
+
// preview exercises the real path. To go live, only this one expression
|
|
48
|
+
// changes — `mockResponse(text)` becomes a POST to your route, with
|
|
49
|
+
// toOpenAIMessages(chat.messages) as the body. The line below stays.
|
|
50
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
51
|
+
stream.done();
|
|
52
|
+
},
|
|
53
|
+
[chat],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={`app${theme === 'dark' ? ' dark' : ''}`}>
|
|
58
|
+
{/* <kai-resizable> owns the sidebar width + the divider. The handle defaults
|
|
59
|
+
to the new `line` hairline (transparent at rest, tinting on hover/drag);
|
|
60
|
+
collapsing the sidebar maps to <ResizableItem collapsed>. */}
|
|
61
|
+
<Resizable theme={theme} orientation="horizontal">
|
|
62
|
+
<ResizableItem theme={theme} size="280px" min="220px" max="420px" collapsed={collapsed}>
|
|
63
|
+
<Sidebar
|
|
64
|
+
theme={theme}
|
|
65
|
+
conversations={conversations}
|
|
66
|
+
activeId={activeId}
|
|
67
|
+
collapsed={collapsed}
|
|
68
|
+
onSelect={selectConversation}
|
|
69
|
+
onNewChat={newChat}
|
|
70
|
+
onToggle={() => setCollapsed((c) => !c)}
|
|
71
|
+
/>
|
|
72
|
+
</ResizableItem>
|
|
73
|
+
|
|
74
|
+
<ResizableItem theme={theme}>
|
|
75
|
+
<main className="main">
|
|
76
|
+
<header className="bar">
|
|
77
|
+
<div className="bar-left">
|
|
78
|
+
{collapsed && (
|
|
79
|
+
<Button
|
|
80
|
+
theme={theme}
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="icon"
|
|
83
|
+
icon="panel-left"
|
|
84
|
+
label="Show sidebar"
|
|
85
|
+
onClick={() => setCollapsed(false)}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
<span className="brand">@kitn.ai/ui · composed chat</span>
|
|
89
|
+
</div>
|
|
90
|
+
<ThemeToggle theme={theme} onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))} />
|
|
91
|
+
</header>
|
|
92
|
+
|
|
93
|
+
<ThreadView theme={theme} messages={chat.messages} />
|
|
94
|
+
|
|
95
|
+
<Composer
|
|
96
|
+
theme={theme}
|
|
97
|
+
loading={chat.loading}
|
|
98
|
+
suggestions={chat.messages.length <= 1 ? SUGGESTIONS : []}
|
|
99
|
+
triggers={TRIGGERS}
|
|
100
|
+
onSubmit={send}
|
|
101
|
+
onSuggestionClick={send}
|
|
102
|
+
/>
|
|
103
|
+
</main>
|
|
104
|
+
</ResizableItem>
|
|
105
|
+
</Resizable>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -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-message>`, 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: "Map your `messages` to `<Message>` elements inside a scroll area, put a `<PromptInput>` below, and wire `onSubmit` to append + stream. That's exactly what this example does — read `App.tsx`." }] },
|
|
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
|
+
// <PromptInput> 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 import the wrappers + tokens:\n\n```tsx\nimport { Conversations, Message, PromptInput } from '@kitn.ai/ui/react';\nimport '@kitn.ai/ui/theme.tokens.css';\n```",
|
|
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**: `<Conversations>` for the sidebar, a `<Message>` per item in a scroll area, and `<PromptInput>` at the bottom — all wired with React 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,87 @@
|
|
|
1
|
+
import { useCallback, useRef } from 'react';
|
|
2
|
+
import { PromptInput } from '@kitn.ai/ui/react';
|
|
3
|
+
import type { PromptInputProps } from '@kitn.ai/ui/react';
|
|
4
|
+
import type { Theme } from '../App';
|
|
5
|
+
import { useVoiceInput } from '@kitn.ai/ui/react';
|
|
6
|
+
|
|
7
|
+
interface ComposerProps {
|
|
8
|
+
theme: Theme;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
suggestions: string[];
|
|
11
|
+
triggers: PromptInputProps['triggers'];
|
|
12
|
+
onSubmit: (value: string) => void;
|
|
13
|
+
onSuggestionClick: (value: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
|
|
18
|
+
* the element ref, a live text mirror, clear-on-submit, and seeding voice.
|
|
19
|
+
*
|
|
20
|
+
* The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
|
|
21
|
+
* caret. Assigning a plain STRING `value` flips the element into controlled mode,
|
|
22
|
+
* which re-applies the property and collapses the shadow-DOM selection — that
|
|
23
|
+
* disables the caret-anchored menu. So we NEVER push a string: clear-on-submit goes
|
|
24
|
+
* through the element's `clear()` method, and voice seeds a ComposerDoc (a
|
|
25
|
+
* non-string value is a one-time seed that leaves the input uncontrolled). We keep
|
|
26
|
+
* the live text mirrored into `textRef` (from `onValueChange`) so voice can append.
|
|
27
|
+
*/
|
|
28
|
+
export function Composer({ theme, loading, suggestions, triggers, onSubmit, onSuggestionClick }: ComposerProps) {
|
|
29
|
+
// Ref to the underlying <kai-prompt-input> element for the imperative pushes
|
|
30
|
+
// (clear() on submit, ComposerDoc seed from voice); textRef mirrors the live text.
|
|
31
|
+
const promptRef = useRef<HTMLElement>(null);
|
|
32
|
+
const textRef = useRef('');
|
|
33
|
+
|
|
34
|
+
// Voice: on a transcript, append it to the current text and seed the
|
|
35
|
+
// (uncontrolled) input as a ComposerDoc. A non-string `value` is a one-time seed
|
|
36
|
+
// into the element's internal state; a plain STRING would instead flip it into
|
|
37
|
+
// controlled mode and re-break the `/`+`@` triggers and submit.
|
|
38
|
+
const { supported: voiceSupported, start: startVoice } = useVoiceInput((transcript) => {
|
|
39
|
+
const next = (textRef.current ? textRef.current + ' ' : '') + transcript;
|
|
40
|
+
textRef.current = next;
|
|
41
|
+
const el = promptRef.current as (HTMLElement & { value?: unknown }) | null;
|
|
42
|
+
if (el) el.value = [{ type: 'text', text: next }];
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const handleVoice = useCallback(() => {
|
|
46
|
+
// Mic is Chromium-only; degrade gracefully everywhere else.
|
|
47
|
+
if (!voiceSupported) {
|
|
48
|
+
alert('Voice input needs a Chromium browser.');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
startVoice();
|
|
52
|
+
}, [voiceSupported, startVoice]);
|
|
53
|
+
|
|
54
|
+
const handleSubmit = useCallback(
|
|
55
|
+
(value: string) => {
|
|
56
|
+
// Clear our own uncontrolled input first — reset the mirror + call the
|
|
57
|
+
// element's clear() method (uncontrolled-safe, it fires kai-value-change
|
|
58
|
+
// internally; never a string assignment) — then hand the text up. The parent
|
|
59
|
+
// owns append + streaming.
|
|
60
|
+
textRef.current = '';
|
|
61
|
+
(promptRef.current as (HTMLElement & { clear?: () => void }) | null)?.clear?.();
|
|
62
|
+
onSubmit(value);
|
|
63
|
+
},
|
|
64
|
+
[onSubmit],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="composer">
|
|
69
|
+
<PromptInput
|
|
70
|
+
ref={promptRef}
|
|
71
|
+
theme={theme}
|
|
72
|
+
placeholder="Message the demo…"
|
|
73
|
+
loading={loading}
|
|
74
|
+
suggestions={suggestions}
|
|
75
|
+
triggers={triggers}
|
|
76
|
+
voice
|
|
77
|
+
onVoice={handleVoice}
|
|
78
|
+
onValueChange={(e) => { textRef.current = e.detail.value; }}
|
|
79
|
+
onSubmit={(e) => handleSubmit(e.detail.value)}
|
|
80
|
+
onSuggestionClick={(e) => onSuggestionClick(e.detail.value)}
|
|
81
|
+
/>
|
|
82
|
+
<p className="composer-hint">
|
|
83
|
+
Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
|
|
84
|
+
</p>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Conversations } from '@kitn.ai/ui/react';
|
|
2
|
+
import type { Theme } from '../App';
|
|
3
|
+
import type { Conversation } from '../chat-data';
|
|
4
|
+
|
|
5
|
+
interface SidebarProps {
|
|
6
|
+
theme: Theme;
|
|
7
|
+
conversations: Conversation[];
|
|
8
|
+
activeId: string;
|
|
9
|
+
collapsed: boolean;
|
|
10
|
+
onSelect: (id: string) => void;
|
|
11
|
+
onNewChat: () => void;
|
|
12
|
+
onToggle: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The conversation rail — a thin wrapper over `<kai-conversations>`. The `.sidebar` div
|
|
17
|
+
* owns the shell's right border (kept OFF the element so it follows the shell's
|
|
18
|
+
* light/dark tokens, not the light-mode ones the element pins on itself at `:host`).
|
|
19
|
+
* The rail's `collapsed` is CONTROLLED by the app's collapsed state, so it stays in
|
|
20
|
+
* sync with the parent `<ResizableItem collapsed>` and re-expands the list on restore;
|
|
21
|
+
* `onToggleSidebar` still reports the toggle intent up via `onToggle`.
|
|
22
|
+
*/
|
|
23
|
+
export function Sidebar({ theme, conversations, activeId, collapsed, onSelect, onNewChat, onToggle }: SidebarProps) {
|
|
24
|
+
return (
|
|
25
|
+
<aside className="sidebar">
|
|
26
|
+
<Conversations
|
|
27
|
+
theme={theme}
|
|
28
|
+
groups={[]} /* flat list: the element buckets `conversations` by recency itself */
|
|
29
|
+
conversations={conversations}
|
|
30
|
+
activeId={activeId}
|
|
31
|
+
collapsed={collapsed}
|
|
32
|
+
onConversationSelect={(e) => onSelect(e.detail.id)}
|
|
33
|
+
onNewChat={onNewChat}
|
|
34
|
+
onToggleSidebar={onToggle}
|
|
35
|
+
/>
|
|
36
|
+
</aside>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Button } from '@kitn.ai/ui/react';
|
|
2
|
+
import type { Theme } from '../App';
|
|
3
|
+
import { MoonIcon, SunIcon } from './icons';
|
|
4
|
+
|
|
5
|
+
interface ThemeToggleProps {
|
|
6
|
+
theme: Theme;
|
|
7
|
+
onToggle: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Light/dark switch for the top bar. Shows the moon in light mode (tap → dark) and
|
|
12
|
+
* the sun in dark mode. The `<Button>` is icon-only, so it carries the accessible
|
|
13
|
+
* label; the glyph is decorative (`aria-hidden`) and sits in the `icon` slot.
|
|
14
|
+
*/
|
|
15
|
+
export function ThemeToggle({ theme, onToggle }: ThemeToggleProps) {
|
|
16
|
+
const Glyph = theme === 'light' ? MoonIcon : SunIcon;
|
|
17
|
+
return (
|
|
18
|
+
<Button theme={theme} variant="ghost" size="icon" label="Toggle light/dark theme" onClick={onToggle}>
|
|
19
|
+
<Glyph slot="icon" aria-hidden />
|
|
20
|
+
</Button>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Thread } from '@kitn.ai/ui/react';
|
|
2
|
+
import type { ChatMessage } from '@kitn.ai/ui/react';
|
|
3
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
4
|
+
import type { Theme } from '../App';
|
|
5
|
+
|
|
6
|
+
interface ThreadViewProps {
|
|
7
|
+
theme: Theme;
|
|
8
|
+
messages: ChatMessage[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The scrolling message list. `<kai-thread>` (React `<Thread>`) owns the message
|
|
13
|
+
* rendering, the centered fixed-width column, and stick-to-bottom scroll, so this
|
|
14
|
+
* component just bakes the per-message actions onto the assistant turns and wires
|
|
15
|
+
* the custom `speak` action to the browser's speech synthesis. `copy` (and the
|
|
16
|
+
* feedback votes) are handled inside the element.
|
|
17
|
+
*/
|
|
18
|
+
export function ThreadView({ theme, messages }: ThreadViewProps) {
|
|
19
|
+
// <Thread> reads `actions` off each message; only assistant replies get them.
|
|
20
|
+
const withActions: ChatMessage[] = messages.map((m) =>
|
|
21
|
+
m.role === 'assistant'
|
|
22
|
+
? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
|
|
23
|
+
: m,
|
|
24
|
+
);
|
|
25
|
+
return (
|
|
26
|
+
<Thread
|
|
27
|
+
className="thread"
|
|
28
|
+
theme={theme}
|
|
29
|
+
messages={withActions}
|
|
30
|
+
onMessageAction={(e) => {
|
|
31
|
+
if (e.detail.action === 'speak') {
|
|
32
|
+
const m = messages.find((x) => x.id === e.detail.messageId);
|
|
33
|
+
if (!m) return;
|
|
34
|
+
window.speechSynthesis.cancel();
|
|
35
|
+
window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
|
|
36
|
+
}
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Moon glyph — shown in light mode (tap the toggle → dark). moon/sun aren't in the
|
|
5
|
+
* kit's `<Icon>` set, so the example owns them. Props spread straight onto the
|
|
6
|
+
* `<svg>`, so `slot="icon"`, `className`, and `aria-hidden` pass through and it
|
|
7
|
+
* drops into `<Button>`'s icon slot.
|
|
8
|
+
*/
|
|
9
|
+
export function MoonIcon({ size = 20, ...props }: { size?: number } & SVGProps<SVGSVGElement>) {
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
viewBox="0 0 24 24"
|
|
13
|
+
width={size}
|
|
14
|
+
height={size}
|
|
15
|
+
fill="none"
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
strokeWidth={2}
|
|
18
|
+
strokeLinecap="round"
|
|
19
|
+
strokeLinejoin="round"
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sun glyph — shown in dark mode (tap the toggle → light). See `MoonIcon` for why
|
|
5
|
+
* the example owns these icons. Props spread straight onto the `<svg>`.
|
|
6
|
+
*/
|
|
7
|
+
export function SunIcon({ size = 20, ...props }: { size?: number } & SVGProps<SVGSVGElement>) {
|
|
8
|
+
return (
|
|
9
|
+
<svg
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
width={size}
|
|
12
|
+
height={size}
|
|
13
|
+
fill="none"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
strokeWidth={2}
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<circle cx="12" cy="12" r="4" />
|
|
21
|
+
<path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" />
|
|
22
|
+
<path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
|
|
23
|
+
</svg>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useConversations } from './useConversations';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import type { KaiChatController } from '@kitn.ai/ui/react';
|
|
3
|
+
import { THREADS, newId, type Conversation } from '../chat-data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Owns the conversation list + which one is active, plus the in-memory `THREADS`
|
|
7
|
+
* stash: on select/new we swap the OPEN thread out to `THREADS` and swap the picked
|
|
8
|
+
* one in. Takes the `useKaiChat` controller so it can read the live messages when
|
|
9
|
+
* stashing and reset them when loading another thread.
|
|
10
|
+
*/
|
|
11
|
+
export function useConversations(chat: KaiChatController, initial: Conversation[]) {
|
|
12
|
+
const [conversations, setConversations] = useState<Conversation[]>(initial);
|
|
13
|
+
const [activeId, setActiveId] = useState(initial[0].id);
|
|
14
|
+
|
|
15
|
+
const selectConversation = useCallback(
|
|
16
|
+
(id: string) => {
|
|
17
|
+
THREADS[activeId] = chat.messages; // stash the open thread
|
|
18
|
+
setActiveId(id);
|
|
19
|
+
chat.setMessages(THREADS[id] ?? []); // load the picked one
|
|
20
|
+
},
|
|
21
|
+
[activeId, chat],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const newChat = useCallback(() => {
|
|
25
|
+
const id = newId();
|
|
26
|
+
THREADS[activeId] = chat.messages;
|
|
27
|
+
THREADS[id] = [];
|
|
28
|
+
setConversations((cs) => [
|
|
29
|
+
{ id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
30
|
+
...cs,
|
|
31
|
+
]);
|
|
32
|
+
setActiveId(id);
|
|
33
|
+
chat.setMessages([]);
|
|
34
|
+
}, [activeId, chat]);
|
|
35
|
+
|
|
36
|
+
return { conversations, activeId, selectConversation, newChat };
|
|
37
|
+
}
|