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,111 @@
|
|
|
1
|
+
html,
|
|
2
|
+
body,
|
|
3
|
+
#root {
|
|
4
|
+
height: 100%;
|
|
5
|
+
margin: 0;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
body {
|
|
9
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* Full-height workspace shell. Colors come from the kit's --color-* tokens, which
|
|
13
|
+
flip to dark under the `.dark` class we toggle on `.app`. <kai-resizable> fills
|
|
14
|
+
this box (its `:host` is `height:100%`) and owns the sidebar | main split, the
|
|
15
|
+
panel widths, and the draggable divider. */
|
|
16
|
+
.app {
|
|
17
|
+
height: 100dvh;
|
|
18
|
+
background: var(--color-background);
|
|
19
|
+
color: var(--color-foreground);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* The sidebar is its own surface: `--color-muted` sits a step off `--color-background`
|
|
23
|
+
in both themes (near-white grey in light, a shade lighter than the base in dark). That
|
|
24
|
+
#f4f4f5-on-white step is subtle, so a 1px right border marks the at-rest sidebar | main
|
|
25
|
+
division: <kai-resizable>'s `line` handle is transparent at rest and only tints on
|
|
26
|
+
hover/drag, so the boundary has to come from the panel itself. The border uses
|
|
27
|
+
`var(--color-border)`, which now tracks the shell's light/dark tokens because the parent
|
|
28
|
+
<ResizableItem theme={theme}> forwards the theme (a light hairline in light mode, dark in
|
|
29
|
+
dark). `box-sizing: border-box` keeps the 1px inside the 280px item so it doesn't shift
|
|
30
|
+
the content width. Collapse is handled by the parent <ResizableItem collapsed> (the panel
|
|
31
|
+
+ its divider drop out), so the old `.sidebar.collapsed` rule is gone. */
|
|
32
|
+
.sidebar {
|
|
33
|
+
background: var(--color-muted);
|
|
34
|
+
border-right: 1px solid var(--color-border);
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
height: 100%;
|
|
37
|
+
min-height: 0;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.sidebar > * {
|
|
42
|
+
display: block;
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.main {
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
min-width: 0;
|
|
51
|
+
height: 100%;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.bar {
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: space-between;
|
|
58
|
+
padding: 0.75rem 1rem;
|
|
59
|
+
border-bottom: 1px solid var(--color-border);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.bar-left {
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 0.5rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.brand {
|
|
69
|
+
font-size: 0.875rem;
|
|
70
|
+
font-weight: 600;
|
|
71
|
+
color: var(--color-muted-foreground);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* <kai-thread> scrolls internally and centers its own fixed-width column, so the
|
|
75
|
+
host just needs to fill the remaining height of the main flex column. */
|
|
76
|
+
.thread {
|
|
77
|
+
flex: 1;
|
|
78
|
+
min-height: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* No top divider above the input — the composer floats against the thread. Extra
|
|
82
|
+
bottom padding lifts the input + hint off the viewport edge. */
|
|
83
|
+
.composer {
|
|
84
|
+
padding: 0.75rem 1rem 1.5rem;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* Match the composer to the centered message column above it. */
|
|
88
|
+
.composer > * {
|
|
89
|
+
display: block;
|
|
90
|
+
width: 100%;
|
|
91
|
+
max-width: 48rem;
|
|
92
|
+
margin: 0 auto;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* Muted, centered helper line under the input pointing out the / and @ triggers. */
|
|
96
|
+
.composer-hint {
|
|
97
|
+
margin: 0.5rem auto 0;
|
|
98
|
+
font-size: 0.75rem;
|
|
99
|
+
text-align: center;
|
|
100
|
+
color: var(--color-muted-foreground);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.composer-hint kbd {
|
|
104
|
+
font-family: inherit;
|
|
105
|
+
font-size: 0.6875rem;
|
|
106
|
+
padding: 0.05rem 0.3rem;
|
|
107
|
+
border-radius: 0.25rem;
|
|
108
|
+
border: 1px solid var(--color-border);
|
|
109
|
+
background: var(--color-muted);
|
|
110
|
+
color: var(--color-foreground);
|
|
111
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import '@kitn.ai/ui/elements'; // register the kai-* custom elements
|
|
4
|
+
import '@kitn.ai/ui/theme.tokens.css'; // plain --color-* tokens for the shell (NOT the tailwind source)
|
|
5
|
+
import './index.css';
|
|
6
|
+
import App from './App';
|
|
7
|
+
|
|
8
|
+
const root = document.getElementById('root');
|
|
9
|
+
if (!root) throw new Error('Root element not found');
|
|
10
|
+
|
|
11
|
+
createRoot(root).render(
|
|
12
|
+
<StrictMode>
|
|
13
|
+
<App />
|
|
14
|
+
</StrictMode>,
|
|
15
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2020",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* React JSX */
|
|
18
|
+
"jsx": "react-jsx",
|
|
19
|
+
|
|
20
|
+
/* Linting */
|
|
21
|
+
"strict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["vite.config.ts"]
|
|
25
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
|
|
4
|
+
// `@kitn.ai/ui` is linked into this example via `workspace:*`, so it resolves
|
|
5
|
+
// through node_modules + the package's `exports` map exactly like a published
|
|
6
|
+
// consumer would — no aliases needed. Build the kit first (`nx build ui`).
|
|
7
|
+
// https://vite.dev/config/
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [react()],
|
|
10
|
+
server: { port: 5173 },
|
|
11
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
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" />
|
|
6
|
+
<title>@kitn.ai/ui SolidJS example</title>
|
|
7
|
+
<!-- No favicon on purpose: this is your app, so the branding is yours. Add one to public/ and link it here. -->
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitn.ai/ui-example-solid",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"preview": "vite preview",
|
|
10
|
+
"typecheck": "tsc"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@kitn.ai/ui": "workspace:*",
|
|
14
|
+
"solid-js": "^1.9.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
18
|
+
"tailwindcss": "^4.2.2",
|
|
19
|
+
"typescript": "^5.5.0",
|
|
20
|
+
"vite": "^6.0.0",
|
|
21
|
+
"vite-plugin-solid": "^2.11.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createMemo, createSignal, Show } from 'solid-js';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
ChatConfig,
|
|
5
|
+
ResizableHandle,
|
|
6
|
+
ResizablePanel,
|
|
7
|
+
ResizablePanelGroup,
|
|
8
|
+
} from '@kitn.ai/ui/solid';
|
|
9
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
10
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
11
|
+
import { CONVERSATIONS, GROUPS, SUGGESTIONS, THREADS, mockResponse, newId } from './chat-data';
|
|
12
|
+
import { createChat } from './lib/chat';
|
|
13
|
+
import { createConversations } from './lib/conversations';
|
|
14
|
+
import type { Theme } from './lib/types';
|
|
15
|
+
import { Composer } from './components/Composer';
|
|
16
|
+
import { Sidebar } from './components/Sidebar';
|
|
17
|
+
import { ThemeToggle } from './components/ThemeToggle';
|
|
18
|
+
import { ThreadView } from './components/ThreadView';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A mini chat **workspace composed by hand** from @kitn.ai/ui — to show how the
|
|
22
|
+
* pieces fit together (vs. dropping in one batteries-included `<kai-chat>`):
|
|
23
|
+
*
|
|
24
|
+
* <ResizablePanelGroup>/<ResizablePanel> — the draggable sidebar | main split
|
|
25
|
+
* <ConversationList> — the sidebar rail (see components/Sidebar.tsx)
|
|
26
|
+
* <ChatContainer> + <Message>/<MessageBody> — the scrolling thread
|
|
27
|
+
* (components/ThreadView.tsx)
|
|
28
|
+
* <PromptInput> — the composer at the bottom (components/Composer.tsx)
|
|
29
|
+
*
|
|
30
|
+
* WHAT MAKES THIS ONE DIFFERENT FROM THE OTHER STARTERS. The React, Vue, Svelte,
|
|
31
|
+
* Angular and vanilla starters consume the `kai-*` WEB COMPONENTS — one custom
|
|
32
|
+
* element per region, with arrays set as JS properties. This one imports the
|
|
33
|
+
* SolidJS components DIRECTLY from `@kitn.ai/ui/solid`, because Solid is the
|
|
34
|
+
* kit's authored layer: there is no shadow root, no custom-element registration,
|
|
35
|
+
* and no property-vs-attribute question. The thread is spelled out as components
|
|
36
|
+
* rather than hidden behind `<kai-thread>`, which is the whole reason to read
|
|
37
|
+
* this one. Import from `@kitn.ai/ui/solid` and never the root entry — `/solid`
|
|
38
|
+
* is a strict superset, and the root ships none of the Solid catalog.
|
|
39
|
+
*
|
|
40
|
+
* The pieces are split into `components/` (the UI subcomponents + the example's
|
|
41
|
+
* own icons) and `lib/` (`createChat` owns the message array + streaming,
|
|
42
|
+
* `createConversations` the conversation stash). `lib/` rather than React's
|
|
43
|
+
* `hooks/`, and `createX` rather than `useX`, because that is what these are in
|
|
44
|
+
* Solid — they run ONCE and hand back accessors, so calling them hooks would
|
|
45
|
+
* name a React lifecycle Solid does not have. Same shape as the Svelte starter's
|
|
46
|
+
* `lib/`.
|
|
47
|
+
*
|
|
48
|
+
* Swap `mockResponse(text)` for a real `fetch` to ship a real app.
|
|
49
|
+
*/
|
|
50
|
+
export default function App() {
|
|
51
|
+
const [theme, setTheme] = createSignal<Theme>('dark');
|
|
52
|
+
const [collapsed, setCollapsed] = createSignal(false);
|
|
53
|
+
const chat = createChat(THREADS[CONVERSATIONS[0].id] ?? []);
|
|
54
|
+
const conversations = createConversations(chat, CONVERSATIONS);
|
|
55
|
+
|
|
56
|
+
// Suggestions are a first-turn affordance: they fall away once the thread is
|
|
57
|
+
// underway rather than sitting under a long conversation.
|
|
58
|
+
const suggestions = createMemo(() => (chat.messages().length <= 1 ? SUGGESTIONS : []));
|
|
59
|
+
|
|
60
|
+
const activeTitle = createMemo(
|
|
61
|
+
() => conversations.conversations().find((c) => c.id === conversations.activeId())?.title ?? '',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
async function send(raw: string) {
|
|
65
|
+
const text = raw.trim();
|
|
66
|
+
if (!text) return;
|
|
67
|
+
// The Composer already cleared its own input; here we just append the user
|
|
68
|
+
// message and stream the (mock) assistant reply.
|
|
69
|
+
chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
70
|
+
const stream = chat.streamAssistant();
|
|
71
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that
|
|
72
|
+
// go through the SAME reader a real model's response would, so this preview
|
|
73
|
+
// exercises the real path. To go live, only this one expression changes —
|
|
74
|
+
// `mockResponse(text)` becomes a POST to your route, with
|
|
75
|
+
// toOpenAIMessages(chat.messages()) as the body. The line below stays.
|
|
76
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
77
|
+
stream.done();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Regenerate: drop the assistant turn (and anything after it), then re-send
|
|
82
|
+
* the user turn that preceded it through the same path above. Re-running
|
|
83
|
+
* `send` rather than patching the old message in place is what keeps this
|
|
84
|
+
* honest — a regenerated reply goes over the same wire as a first one.
|
|
85
|
+
*/
|
|
86
|
+
function regenerate(assistantId: string) {
|
|
87
|
+
if (chat.loading()) return;
|
|
88
|
+
const all = chat.messages();
|
|
89
|
+
const at = all.findIndex((m) => m.id === assistantId);
|
|
90
|
+
if (at < 0) return;
|
|
91
|
+
const prompt = all.slice(0, at).reverse().find((m) => m.role === 'user');
|
|
92
|
+
if (!prompt) return;
|
|
93
|
+
// Truncate to just before the prompt: `send` re-appends it.
|
|
94
|
+
chat.setMessages(all.slice(0, all.indexOf(prompt)));
|
|
95
|
+
void send(partsToText(prompt.parts));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<ChatConfig>
|
|
100
|
+
{/* The kit declares every --color-* token twice and flips the set under a
|
|
101
|
+
`.dark` ancestor (`@custom-variant dark (&:is(.dark *))` in theme.css),
|
|
102
|
+
so one class on the shell themes the components AND your own chrome. */}
|
|
103
|
+
<div class={`h-screen w-full overflow-hidden bg-background${theme() === 'dark' ? ' dark' : ''}`}>
|
|
104
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
105
|
+
<Show when={!collapsed()}>
|
|
106
|
+
<ResizablePanel defaultSize="280px" minSize="220px" maxSize="420px">
|
|
107
|
+
<Sidebar
|
|
108
|
+
groups={GROUPS}
|
|
109
|
+
conversations={conversations.conversations}
|
|
110
|
+
activeId={conversations.activeId}
|
|
111
|
+
onSelect={conversations.selectConversation}
|
|
112
|
+
onNewChat={conversations.newChat}
|
|
113
|
+
onToggleSidebar={() => setCollapsed(true)}
|
|
114
|
+
/>
|
|
115
|
+
</ResizablePanel>
|
|
116
|
+
<ResizableHandle handle="line" />
|
|
117
|
+
</Show>
|
|
118
|
+
|
|
119
|
+
<ResizablePanel>
|
|
120
|
+
<main class="flex h-full flex-1 flex-col overflow-hidden">
|
|
121
|
+
<header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
|
|
122
|
+
<div class="flex items-center gap-2">
|
|
123
|
+
<Show when={collapsed()}>
|
|
124
|
+
<Button variant="ghost" size="icon-sm" onClick={() => setCollapsed(false)}>
|
|
125
|
+
<span aria-hidden>☰</span>
|
|
126
|
+
<span class="sr-only">Show sidebar</span>
|
|
127
|
+
</Button>
|
|
128
|
+
</Show>
|
|
129
|
+
<span class="text-sm font-semibold text-foreground">{activeTitle()}</span>
|
|
130
|
+
</div>
|
|
131
|
+
<div class="flex items-center gap-3">
|
|
132
|
+
<span class="hidden text-xs text-muted-foreground sm:inline">
|
|
133
|
+
@kitn.ai/ui · composed chat
|
|
134
|
+
</span>
|
|
135
|
+
<ThemeToggle
|
|
136
|
+
theme={theme}
|
|
137
|
+
onToggle={() => setTheme((t) => (t === 'light' ? 'dark' : 'light'))}
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</header>
|
|
141
|
+
|
|
142
|
+
<ThreadView messages={chat.messages} onRegenerate={regenerate} />
|
|
143
|
+
|
|
144
|
+
<Composer loading={chat.loading} suggestions={suggestions} onSubmit={send} />
|
|
145
|
+
</main>
|
|
146
|
+
</ResizablePanel>
|
|
147
|
+
</ResizablePanelGroup>
|
|
148
|
+
</div>
|
|
149
|
+
</ChatConfig>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// Self-contained sample data + the kit's shared mock responder — no backend.
|
|
2
|
+
//
|
|
3
|
+
// The seeded threads and the conversation rail live here so `App.tsx` reads as
|
|
4
|
+
// composition rather than as a wall of fixture data. Mirrors
|
|
5
|
+
// `examples/starters/react/src/chat-data.ts`; the types are the kit's own, taken
|
|
6
|
+
// from `@kitn.ai/ui/solid` rather than redeclared, because a Solid app renders
|
|
7
|
+
// the components directly and so already has them.
|
|
8
|
+
import type {
|
|
9
|
+
ChatMessage,
|
|
10
|
+
ChatMessageAction,
|
|
11
|
+
ConversationGroup,
|
|
12
|
+
ConversationSummary,
|
|
13
|
+
} from '@kitn.ai/ui/solid';
|
|
14
|
+
import { createMockResponder } from '@kitn.ai/ui/state';
|
|
15
|
+
|
|
16
|
+
export function newId(): string {
|
|
17
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
18
|
+
? crypto.randomUUID()
|
|
19
|
+
: 'id-' + Math.random().toString(36).slice(2);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const iso = (daysAgo = 0) => new Date(Date.now() - daysAgo * 86_400_000).toISOString();
|
|
23
|
+
|
|
24
|
+
const scope = { type: 'document' as const };
|
|
25
|
+
|
|
26
|
+
export const GROUPS: ConversationGroup[] = [
|
|
27
|
+
{ id: 'today', name: 'Today', sortOrder: 0, createdAt: iso(0) },
|
|
28
|
+
{ id: 'yesterday', name: 'Yesterday', sortOrder: 1, createdAt: iso(1) },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export const CONVERSATIONS: ConversationSummary[] = [
|
|
32
|
+
{
|
|
33
|
+
id: 'c1',
|
|
34
|
+
title: 'SolidJS signals vs React hooks',
|
|
35
|
+
groupId: 'today',
|
|
36
|
+
scope,
|
|
37
|
+
messageCount: 6,
|
|
38
|
+
lastMessageAt: iso(0),
|
|
39
|
+
updatedAt: iso(0),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'c2',
|
|
43
|
+
title: 'Composing your own chat',
|
|
44
|
+
groupId: 'today',
|
|
45
|
+
scope,
|
|
46
|
+
messageCount: 2,
|
|
47
|
+
lastMessageAt: iso(0),
|
|
48
|
+
updatedAt: iso(0),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'c3',
|
|
52
|
+
title: 'Theming + dark mode',
|
|
53
|
+
groupId: 'yesterday',
|
|
54
|
+
scope,
|
|
55
|
+
messageCount: 2,
|
|
56
|
+
lastMessageAt: iso(1),
|
|
57
|
+
updatedAt: iso(1),
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
/** The action bar under every assistant message. Built-in names; `<MessageBody>`
|
|
62
|
+
* renders the buttons and reports clicks through `onAction`. */
|
|
63
|
+
export const ASSISTANT_ACTIONS: ChatMessageAction[] = ['copy', 'like', 'dislike', 'regenerate'];
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The in-memory conversation stash, keyed by conversation id.
|
|
67
|
+
*
|
|
68
|
+
* `createConversations` swaps the OPEN thread out to here and the picked one in,
|
|
69
|
+
* so switching conversations does not lose what was already streamed. A real app
|
|
70
|
+
* replaces this object with a fetch.
|
|
71
|
+
*
|
|
72
|
+
* The `c1` thread deliberately ends on a message carrying REASONING and a TOOL
|
|
73
|
+
* part before its text. A message is an ordered list of parts, not a string, and
|
|
74
|
+
* `<MessageBody>` renders each kind in place — flattening to text silently drops
|
|
75
|
+
* both. The mock responder streams text only (it produces an OpenAI-shaped wire,
|
|
76
|
+
* and that wire carries text deltas), so a seeded turn is what keeps the other
|
|
77
|
+
* two part kinds on screen.
|
|
78
|
+
*/
|
|
79
|
+
export const THREADS: Record<string, ChatMessage[]> = {
|
|
80
|
+
c1: [
|
|
81
|
+
{
|
|
82
|
+
id: 'c1u1',
|
|
83
|
+
role: 'user',
|
|
84
|
+
parts: [{ type: 'text', text: 'How does SolidJS reactivity differ from React hooks?' }],
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'c1a1',
|
|
88
|
+
role: 'assistant',
|
|
89
|
+
parts: [
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: `**SolidJS** takes a fundamentally different approach to reactivity.
|
|
93
|
+
|
|
94
|
+
### Signals vs useState
|
|
95
|
+
|
|
96
|
+
In SolidJS, signals are **fine-grained reactive primitives** — when a signal updates, only the DOM nodes that read it are updated; no virtual DOM diff required.
|
|
97
|
+
|
|
98
|
+
\`\`\`tsx
|
|
99
|
+
// SolidJS — runs once, DOM updates surgically
|
|
100
|
+
const [count, setCount] = createSignal(0);
|
|
101
|
+
return <p>{count()}</p>; // only this text node re-renders
|
|
102
|
+
|
|
103
|
+
// React — entire component re-renders
|
|
104
|
+
const [count, setCount] = useState(0);
|
|
105
|
+
return <p>{count}</p>; // whole function re-executes
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
### Key differences
|
|
109
|
+
|
|
110
|
+
1. **No re-renders** — SolidJS components run once; only reactive expressions update
|
|
111
|
+
2. **No dependency arrays** — \`createEffect\` auto-tracks dependencies
|
|
112
|
+
3. **No stale closures** — signals are getter functions, always current`,
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: 'c1u2',
|
|
118
|
+
role: 'user',
|
|
119
|
+
parts: [{ type: 'text', text: "What's the signature of createResource?" }],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: 'c1a2',
|
|
123
|
+
role: 'assistant',
|
|
124
|
+
parts: [
|
|
125
|
+
{
|
|
126
|
+
type: 'reasoning',
|
|
127
|
+
text: "They want the exact signature, not a paraphrase. I should look it up in the reference rather than recall it — the fetcher's second argument changed in 1.5 and I'd rather not guess.",
|
|
128
|
+
label: 'Thinking',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'tool',
|
|
132
|
+
tool: {
|
|
133
|
+
type: 'search_docs',
|
|
134
|
+
kind: 'search',
|
|
135
|
+
state: 'output-available',
|
|
136
|
+
toolCallId: 'call_docs_1',
|
|
137
|
+
input: { query: 'createResource signature', version: '1.9' },
|
|
138
|
+
output: {
|
|
139
|
+
top: 'solidjs.com/docs/latest/api#createresource',
|
|
140
|
+
excerpt:
|
|
141
|
+
'createResource(source?, fetcher, options?) => [Resource, { mutate, refetch }]',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'text',
|
|
147
|
+
text: `\`createResource\` returns a resource accessor plus an actions object.
|
|
148
|
+
|
|
149
|
+
\`\`\`ts
|
|
150
|
+
const [data, { mutate, refetch }] = createResource(source, fetcher);
|
|
151
|
+
|
|
152
|
+
// data() — the value, or undefined while pending
|
|
153
|
+
// data.loading — boolean
|
|
154
|
+
// data.error — the thrown error, if any
|
|
155
|
+
\`\`\`
|
|
156
|
+
|
|
157
|
+
Omit \`source\` for a fetch that runs once; pass a signal to re-fetch whenever it changes.`,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
c2: [
|
|
163
|
+
{
|
|
164
|
+
id: 'c2u',
|
|
165
|
+
role: 'user',
|
|
166
|
+
parts: [
|
|
167
|
+
{ type: 'text', text: 'How do I build my own chat instead of dropping in `<kai-chat>`?' },
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'c2a',
|
|
172
|
+
role: 'assistant',
|
|
173
|
+
parts: [
|
|
174
|
+
{
|
|
175
|
+
type: 'text',
|
|
176
|
+
text: 'Put a `<Message>` per item inside a `<ChatContainer>`, hand each one\'s `parts` to `<MessageBody>`, and wire a `<PromptInput>` below it to append + stream. That is exactly what this example does — read `App.tsx`, then `components/ThreadView.tsx`.',
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
c3: [
|
|
182
|
+
{ id: 'c3u', role: 'user', parts: [{ type: 'text', text: 'How does dark mode work?' }] },
|
|
183
|
+
{
|
|
184
|
+
id: 'c3a',
|
|
185
|
+
role: 'assistant',
|
|
186
|
+
parts: [
|
|
187
|
+
{
|
|
188
|
+
type: 'text',
|
|
189
|
+
text: "The kit ships a `.dark` class variant: `theme.css` declares every `--color-*` token twice, and `@custom-variant dark (&:is(.dark *))` flips the whole tree under it. Drive the class from app state — toggle it with the button top-right — and both the kit's components and your own chrome follow.",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export const SUGGESTIONS = [
|
|
197
|
+
'How do SolidJS stores work?',
|
|
198
|
+
'SolidJS TypeScript tips',
|
|
199
|
+
'Show me some markdown',
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
// The replies the mock cycles through, one per turn. Markdown-rich on purpose:
|
|
203
|
+
// the thread renders markdown, code blocks and blockquotes, so the canned
|
|
204
|
+
// replies exercise that instead of only proving text arrives.
|
|
205
|
+
//
|
|
206
|
+
// The first one says it is a mock in words. That is the WEAKEST of the
|
|
207
|
+
// responder's tells and the only one a real model could imitate, which is
|
|
208
|
+
// exactly why it is not the only one — the rest are on the wire.
|
|
209
|
+
const MOCK_REPLIES = [
|
|
210
|
+
"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.",
|
|
211
|
+
'SolidJS **stores** are deeply reactive objects — good for shared state:\n\n```ts\nimport { createStore } from "solid-js/store";\n\nconst [state, setState] = createStore({ count: 0, user: { name: "Alice" } });\n\n// Fine-grained: only what reads state.count re-runs\nsetState("count", (c) => c + 1);\nsetState("user", "name", "Bob");\n```\n\nFor global state, wrap a store in `createContext` + `useContext`.',
|
|
212
|
+
'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.',
|
|
213
|
+
"This chat is **composed by hand** out of the SolidJS components: `<ConversationList>` for the sidebar, a `<Message>` + `<MessageBody>` per turn inside `<ChatContainer>`, and `<PromptInput>` at the bottom — all wired with `createSignal`. Swap `mockResponse(text)` for your own model call and you're shipping.",
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The kit's own mock responder, shared with the `kai` MCP scaffolder and
|
|
218
|
+
* `create-kai` so there is ONE implementation of this and not seven.
|
|
219
|
+
*
|
|
220
|
+
* It yields canned SSE frames in the OpenAI chat-completions shape, which
|
|
221
|
+
* `readOpenAIStream` parses exactly as it parses a real provider's. So this
|
|
222
|
+
* no-backend preview runs the kit's REAL streaming path — the SSE reader, the
|
|
223
|
+
* part folding, all of it — rather than a hand-rolled loop that merely
|
|
224
|
+
* resembles it. Going live then changes one expression, not the handler.
|
|
225
|
+
*
|
|
226
|
+
* It also cannot be mistaken for a real turn: the stream opens with a
|
|
227
|
+
* `: kai-mock` SSE comment, every frame carries a `_kai_mock` field, `model`
|
|
228
|
+
* reports as `kai-mock` (no provider serves that, so an echoed mock frame is
|
|
229
|
+
* rejected upstream rather than quietly believed), and usage is all zeros.
|
|
230
|
+
*
|
|
231
|
+
* MODULE scope, not per-send: the responder owns the cursor into the replies
|
|
232
|
+
* above, so rebuilding it each turn would answer with the first one forever.
|
|
233
|
+
*/
|
|
234
|
+
export const mockResponse = createMockResponder({ replies: MOCK_REPLIES });
|