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,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"newProjectRoot": "projects",
|
|
5
|
+
"projects": {
|
|
6
|
+
"ui-example-angular": {
|
|
7
|
+
"projectType": "application",
|
|
8
|
+
"root": "",
|
|
9
|
+
"sourceRoot": "src",
|
|
10
|
+
"prefix": "app",
|
|
11
|
+
"architect": {
|
|
12
|
+
"build": {
|
|
13
|
+
"builder": "@angular/build:application",
|
|
14
|
+
"options": {
|
|
15
|
+
"outputPath": "dist/ui-example-angular",
|
|
16
|
+
"index": "src/index.html",
|
|
17
|
+
"browser": "src/main.ts",
|
|
18
|
+
"polyfills": [],
|
|
19
|
+
"tsConfig": "tsconfig.app.json",
|
|
20
|
+
"assets": [],
|
|
21
|
+
"styles": [
|
|
22
|
+
"node_modules/@kitn.ai/ui/dist/theme.tokens.css",
|
|
23
|
+
"src/styles.css"
|
|
24
|
+
],
|
|
25
|
+
"scripts": []
|
|
26
|
+
},
|
|
27
|
+
"configurations": {
|
|
28
|
+
"production": {
|
|
29
|
+
"outputHashing": "all"
|
|
30
|
+
},
|
|
31
|
+
"development": {
|
|
32
|
+
"optimization": false,
|
|
33
|
+
"sourceMap": true
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"defaultConfiguration": "production"
|
|
37
|
+
},
|
|
38
|
+
"serve": {
|
|
39
|
+
"builder": "@angular/build:dev-server",
|
|
40
|
+
"configurations": {
|
|
41
|
+
"production": {
|
|
42
|
+
"buildTarget": "ui-example-angular:build:production"
|
|
43
|
+
},
|
|
44
|
+
"development": {
|
|
45
|
+
"buildTarget": "ui-example-angular:build:development"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"defaultConfiguration": "development"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kitn.ai/ui-example-angular",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": ">=22.22.3"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"ng": "ng",
|
|
10
|
+
"dev": "ng serve",
|
|
11
|
+
"start": "ng serve",
|
|
12
|
+
"build": "ng build",
|
|
13
|
+
"watch": "ng build --watch --configuration development"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@angular/common": "^22.0.0",
|
|
17
|
+
"@angular/compiler": "^22.0.0",
|
|
18
|
+
"@angular/core": "^22.0.0",
|
|
19
|
+
"@angular/platform-browser": "^22.0.0",
|
|
20
|
+
"@kitn.ai/ui": "workspace:*",
|
|
21
|
+
"rxjs": "~7.8.0",
|
|
22
|
+
"tslib": "^2.3.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@angular/build": "^22.0.0",
|
|
26
|
+
"@angular/cli": "^22.0.0",
|
|
27
|
+
"@angular/compiler-cli": "^22.0.0",
|
|
28
|
+
"typescript": "~6.0.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
// Minimal standalone bootstrap config. No router/HTTP needed for the demo; the kit
|
|
4
|
+
// elements register in main.ts before bootstrap. Angular 22 is zoneless by default —
|
|
5
|
+
// no zone.js polyfill. The app is fully signal-based (the chat/conversation stores
|
|
6
|
+
// use signals, components are OnPush-friendly) and the template `(kai-*)` event
|
|
7
|
+
// bindings schedule change detection, so streaming/theme/switch/collapse all update
|
|
8
|
+
// without Zone.
|
|
9
|
+
export const appConfig: ApplicationConfig = {
|
|
10
|
+
providers: [provideZonelessChangeDetection()],
|
|
11
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* Full-height workspace shell. Colors come from the kit's --color-* tokens, which
|
|
2
|
+
flip to dark under the `.dark` class we toggle on `.app`. <kai-resizable> fills
|
|
3
|
+
this box (its `:host` is `height:100%`) and owns the sidebar | main split, the
|
|
4
|
+
panel widths, and the draggable divider. */
|
|
5
|
+
.app {
|
|
6
|
+
height: 100dvh;
|
|
7
|
+
background: var(--color-background);
|
|
8
|
+
color: var(--color-foreground);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.main {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
min-width: 0;
|
|
15
|
+
height: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.bar {
|
|
19
|
+
display: flex;
|
|
20
|
+
align-items: center;
|
|
21
|
+
justify-content: space-between;
|
|
22
|
+
padding: 0.75rem 1rem;
|
|
23
|
+
border-bottom: 1px solid var(--color-border);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.bar-left {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 0.5rem;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.brand {
|
|
33
|
+
font-size: 0.875rem;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
color: var(--color-muted-foreground);
|
|
36
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div class="app" [class.dark]="theme() === 'dark'">
|
|
2
|
+
<!-- <kai-resizable> owns the sidebar width + the divider. The handle defaults to
|
|
3
|
+
the `line` hairline (transparent at rest, tinting on hover/drag); collapsing
|
|
4
|
+
the sidebar maps to <kai-resizable-item [collapsed]>. Pass `theme` to the group
|
|
5
|
+
AND every item so slotted chrome inherits the right light/dark tokens. -->
|
|
6
|
+
<kai-resizable [theme]="theme()" orientation="horizontal">
|
|
7
|
+
<kai-resizable-item [theme]="theme()" size="280px" min="220px" max="420px" [collapsed]="collapsed()">
|
|
8
|
+
<app-sidebar
|
|
9
|
+
[theme]="theme()"
|
|
10
|
+
[conversations]="conversations()"
|
|
11
|
+
[activeId]="activeId()"
|
|
12
|
+
[collapsed]="collapsed()"
|
|
13
|
+
(select)="selectConversation($event)"
|
|
14
|
+
(newChat)="newChat()"
|
|
15
|
+
(toggle)="collapsed.set(!collapsed())"
|
|
16
|
+
></app-sidebar>
|
|
17
|
+
</kai-resizable-item>
|
|
18
|
+
|
|
19
|
+
<kai-resizable-item [theme]="theme()">
|
|
20
|
+
<main class="main">
|
|
21
|
+
<header class="bar">
|
|
22
|
+
<div class="bar-left">
|
|
23
|
+
@if (collapsed()) {
|
|
24
|
+
<kai-button
|
|
25
|
+
[theme]="theme()"
|
|
26
|
+
variant="ghost"
|
|
27
|
+
size="icon"
|
|
28
|
+
icon="panel-left"
|
|
29
|
+
label="Show sidebar"
|
|
30
|
+
(click)="collapsed.set(false)"
|
|
31
|
+
></kai-button>
|
|
32
|
+
}
|
|
33
|
+
<span class="brand">@kitn.ai/ui · composed chat</span>
|
|
34
|
+
</div>
|
|
35
|
+
<app-theme-toggle [theme]="theme()" (toggle)="toggleTheme()"></app-theme-toggle>
|
|
36
|
+
</header>
|
|
37
|
+
|
|
38
|
+
<app-thread-view [theme]="theme()" [messages]="chat.messages()"></app-thread-view>
|
|
39
|
+
|
|
40
|
+
<app-composer
|
|
41
|
+
[theme]="theme()"
|
|
42
|
+
[loading]="chat.loading()"
|
|
43
|
+
[suggestions]="suggestions()"
|
|
44
|
+
[triggers]="triggers"
|
|
45
|
+
(submit)="send($event)"
|
|
46
|
+
(suggestion)="send($event)"
|
|
47
|
+
></app-composer>
|
|
48
|
+
</main>
|
|
49
|
+
</kai-resizable-item>
|
|
50
|
+
</kai-resizable>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA, Component, computed, signal } from '@angular/core';
|
|
2
|
+
import { readOpenAIStream } from '@kitn.ai/ui/wire';
|
|
3
|
+
import { CONVERSATIONS, THREADS, SUGGESTIONS, TRIGGERS, newId, mockResponse } from '../chat-data';
|
|
4
|
+
import type { Theme } from './types';
|
|
5
|
+
import { createChat } from './state/chat.store';
|
|
6
|
+
import { createConversations } from './state/conversations.store';
|
|
7
|
+
import { Sidebar } from './components/sidebar/sidebar';
|
|
8
|
+
import { ThreadView } from './components/thread-view/thread-view';
|
|
9
|
+
import { Composer } from './components/composer/composer';
|
|
10
|
+
import { ThemeToggle } from './components/theme-toggle/theme-toggle';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A mini chat workspace COMPOSED BY HAND from @kitn.ai/ui's individual elements —
|
|
14
|
+
* the Angular mirror of `examples/react` and `examples/vue`. It shows how the raw
|
|
15
|
+
* `kai-*` web components fit together (vs. dropping in one batteries-included
|
|
16
|
+
* `<kai-chat>`/`<kai-workspace>`):
|
|
17
|
+
*
|
|
18
|
+
* <kai-resizable>/<kai-resizable-item> — the draggable sidebar | main split (the
|
|
19
|
+
* divider is the kit's default `line` hairline)
|
|
20
|
+
* <kai-conversations> — the sidebar list (fed `conversations`, emits select/new)
|
|
21
|
+
* <kai-thread> — the scrolling message list (stick-to-bottom built in)
|
|
22
|
+
* <kai-prompt-input> — the composer at the bottom
|
|
23
|
+
*
|
|
24
|
+
* The pieces are split into standalone sub-components + the example's own moon/sun
|
|
25
|
+
* icons, and `state/` (`createChat` owns the message array + streaming,
|
|
26
|
+
* `createConversations` the conversation stash, `useVoiceInput` the mic). Everything
|
|
27
|
+
* else is plain Angular signals. Swap `mockResponse(text)` for a real `fetch` to ship.
|
|
28
|
+
*
|
|
29
|
+
* `CUSTOM_ELEMENTS_SCHEMA` tells Angular to allow the unknown `kai-*` tags and pass
|
|
30
|
+
* property/event bindings straight through to the DOM.
|
|
31
|
+
*/
|
|
32
|
+
@Component({
|
|
33
|
+
selector: 'app-root',
|
|
34
|
+
imports: [Sidebar, ThreadView, Composer, ThemeToggle],
|
|
35
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
36
|
+
templateUrl: './app.html',
|
|
37
|
+
styleUrl: './app.css',
|
|
38
|
+
})
|
|
39
|
+
export class App {
|
|
40
|
+
readonly theme = signal<Theme>('dark');
|
|
41
|
+
readonly collapsed = signal(false);
|
|
42
|
+
|
|
43
|
+
readonly chat = createChat(THREADS[CONVERSATIONS[0].id] ?? []);
|
|
44
|
+
private readonly convos = createConversations(this.chat, CONVERSATIONS);
|
|
45
|
+
readonly conversations = this.convos.conversations;
|
|
46
|
+
readonly activeId = this.convos.activeId;
|
|
47
|
+
|
|
48
|
+
readonly suggestions = computed(() => (this.chat.messages().length <= 1 ? SUGGESTIONS : []));
|
|
49
|
+
readonly triggers = TRIGGERS;
|
|
50
|
+
|
|
51
|
+
toggleTheme() {
|
|
52
|
+
this.theme.set(this.theme() === 'light' ? 'dark' : 'light');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
selectConversation(id: string) {
|
|
56
|
+
this.convos.selectConversation(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
newChat() {
|
|
60
|
+
this.convos.newChat();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async send(raw: string) {
|
|
64
|
+
const text = raw.trim();
|
|
65
|
+
if (!text) return;
|
|
66
|
+
// The Composer already cleared its own input; here we just append the user
|
|
67
|
+
// message and stream the (mock) assistant reply.
|
|
68
|
+
this.chat.append({ id: newId(), role: 'user', parts: [{ type: 'text', text }] });
|
|
69
|
+
const stream = this.chat.streamAssistant();
|
|
70
|
+
// NO BACKEND AND NO PROVIDER. mockResponse() yields canned SSE frames that go
|
|
71
|
+
// through the SAME reader a real model's response would, so this preview
|
|
72
|
+
// exercises the real path. To go live, only this one expression changes —
|
|
73
|
+
// `mockResponse(text)` becomes a POST to your route, with
|
|
74
|
+
// toOpenAIMessages(this.chat.messages()) as the body. The line below stays.
|
|
75
|
+
await readOpenAIStream(mockResponse(text), stream);
|
|
76
|
+
stream.done();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* No top divider above the input — the composer floats against the thread. Extra
|
|
2
|
+
bottom padding lifts the input + hint off the viewport edge. */
|
|
3
|
+
.composer {
|
|
4
|
+
padding: 0.75rem 1rem 1.5rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/* Match the composer to the centered message column above it. */
|
|
8
|
+
.composer > * {
|
|
9
|
+
display: block;
|
|
10
|
+
width: 100%;
|
|
11
|
+
max-width: 48rem;
|
|
12
|
+
margin: 0 auto;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Muted, centered helper line under the input pointing out the / and @ triggers. */
|
|
16
|
+
.composer-hint {
|
|
17
|
+
margin: 0.5rem auto 0;
|
|
18
|
+
font-size: 0.75rem;
|
|
19
|
+
text-align: center;
|
|
20
|
+
color: var(--color-muted-foreground);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.composer-hint kbd {
|
|
24
|
+
font-family: inherit;
|
|
25
|
+
font-size: 0.6875rem;
|
|
26
|
+
padding: 0.05rem 0.3rem;
|
|
27
|
+
border-radius: 0.25rem;
|
|
28
|
+
border: 1px solid var(--color-border);
|
|
29
|
+
background: var(--color-muted);
|
|
30
|
+
color: var(--color-foreground);
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<div class="composer">
|
|
2
|
+
<kai-prompt-input
|
|
3
|
+
#prompt
|
|
4
|
+
[theme]="theme()"
|
|
5
|
+
placeholder="Message the demo…"
|
|
6
|
+
[loading]="loading()"
|
|
7
|
+
[suggestions]="suggestions()"
|
|
8
|
+
[triggers]="triggers()"
|
|
9
|
+
[voice]="true"
|
|
10
|
+
(kai-voice)="handleVoice()"
|
|
11
|
+
(kai-value-change)="onValueChange($event)"
|
|
12
|
+
(kai-submit)="onSubmit($event)"
|
|
13
|
+
(kai-suggestion-click)="onSuggestionClick($event)"
|
|
14
|
+
></kai-prompt-input>
|
|
15
|
+
<p class="composer-hint">
|
|
16
|
+
Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA, Component, ElementRef, ViewChild, input, output } from '@angular/core';
|
|
2
|
+
import type { Theme } from '../../types';
|
|
3
|
+
import { useVoiceInput } from '../../state/voice-input';
|
|
4
|
+
|
|
5
|
+
/** The imperative surface of <kai-prompt-input> this composer drives. */
|
|
6
|
+
type PromptInputEl = HTMLElement & { value?: unknown; clear?: () => void };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
|
|
10
|
+
* the element ref, a live text mirror, clear-on-submit, and seeding voice.
|
|
11
|
+
*
|
|
12
|
+
* The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
|
|
13
|
+
* caret. We NEVER bind `[value]` with a plain STRING: that flips the element into
|
|
14
|
+
* controlled mode, which re-applies the property and collapses the shadow-DOM
|
|
15
|
+
* selection, disabling the caret-anchored menu. So clear-on-submit goes through the
|
|
16
|
+
* element's `clear()` method, and voice seeds a ComposerDoc (a non-string value is a
|
|
17
|
+
* one-time seed that leaves the input uncontrolled). We keep the live text mirrored
|
|
18
|
+
* in `liveText` (from `kai-value-change`) so voice can append.
|
|
19
|
+
*
|
|
20
|
+
* `voice` is set as a truthy PROPERTY (`[voice]="true"`) because the facade's flag()
|
|
21
|
+
* takes an explicit JS `false` over a present attribute, so the bound property is the
|
|
22
|
+
* one that can still turn the flag back off. A bare `voice` attribute would also read
|
|
23
|
+
* as TRUE here — presence is what counts, and only `voice="false"` reads as false.
|
|
24
|
+
*/
|
|
25
|
+
@Component({
|
|
26
|
+
selector: 'app-composer',
|
|
27
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
28
|
+
templateUrl: './composer.html',
|
|
29
|
+
styleUrl: './composer.css',
|
|
30
|
+
})
|
|
31
|
+
export class Composer {
|
|
32
|
+
theme = input.required<Theme>();
|
|
33
|
+
loading = input.required<boolean>();
|
|
34
|
+
suggestions = input.required<string[]>();
|
|
35
|
+
triggers = input.required<unknown[]>();
|
|
36
|
+
|
|
37
|
+
submit = output<string>();
|
|
38
|
+
suggestion = output<string>();
|
|
39
|
+
|
|
40
|
+
// Ref to the underlying <kai-prompt-input> element for the imperative pushes
|
|
41
|
+
// (clear() on submit, ComposerDoc seed from voice); `liveText` mirrors the text.
|
|
42
|
+
@ViewChild('prompt', { static: true }) promptRef!: ElementRef<PromptInputEl>;
|
|
43
|
+
private liveText = '';
|
|
44
|
+
|
|
45
|
+
// Voice: on a transcript, append it to the current text and seed the (uncontrolled)
|
|
46
|
+
// input as a ComposerDoc. A non-string `value` is a one-time seed into the
|
|
47
|
+
// element's internal state; a plain STRING would instead flip it into controlled
|
|
48
|
+
// mode and re-break the `/`+`@` triggers and submit.
|
|
49
|
+
private readonly voice = useVoiceInput((transcript) => {
|
|
50
|
+
const next = (this.liveText ? this.liveText + ' ' : '') + transcript;
|
|
51
|
+
this.liveText = next;
|
|
52
|
+
const el = this.promptRef?.nativeElement;
|
|
53
|
+
if (el) el.value = [{ type: 'text', text: next }];
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
handleVoice() {
|
|
57
|
+
// Mic is Chromium-only; degrade gracefully everywhere else.
|
|
58
|
+
if (!this.voice.supported) {
|
|
59
|
+
alert('Voice input needs a Chromium browser.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
this.voice.start();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onValueChange(e: Event) {
|
|
66
|
+
this.liveText = (e as CustomEvent<{ value: string }>).detail.value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onSubmit(e: Event) {
|
|
70
|
+
// Clear our own uncontrolled input first — reset the mirror + call the element's
|
|
71
|
+
// clear() method (uncontrolled-safe; fires kai-value-change internally, never a
|
|
72
|
+
// string assignment) — then hand the text up. The parent owns append + streaming.
|
|
73
|
+
this.liveText = '';
|
|
74
|
+
this.promptRef?.nativeElement?.clear?.();
|
|
75
|
+
this.submit.emit((e as CustomEvent<{ value: string }>).detail.value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onSuggestionClick(e: Event) {
|
|
79
|
+
this.suggestion.emit((e as CustomEvent<{ value: string }>).detail.value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
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. The host is `display: inline-flex` so
|
|
6
|
+
* the 20x20 svg it wraps drops cleanly into `<kai-button>`'s icon slot (the parent
|
|
7
|
+
* puts `slot="icon"` on this element).
|
|
8
|
+
*/
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'app-moon-icon',
|
|
11
|
+
standalone: true,
|
|
12
|
+
templateUrl: './moon-icon.html',
|
|
13
|
+
styleUrl: './moon-icon.css',
|
|
14
|
+
})
|
|
15
|
+
export class MoonIcon {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
width="20"
|
|
3
|
+
height="20"
|
|
4
|
+
viewBox="0 0 24 24"
|
|
5
|
+
fill="none"
|
|
6
|
+
stroke="currentColor"
|
|
7
|
+
stroke-width="2"
|
|
8
|
+
stroke-linecap="round"
|
|
9
|
+
stroke-linejoin="round"
|
|
10
|
+
>
|
|
11
|
+
<circle cx="12" cy="12" r="4" />
|
|
12
|
+
<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" />
|
|
13
|
+
<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" />
|
|
14
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Component } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sun glyph — shown in dark mode (tap the toggle -> light). See MoonIcon for why the
|
|
5
|
+
* example owns these icons.
|
|
6
|
+
*/
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'app-sun-icon',
|
|
9
|
+
standalone: true,
|
|
10
|
+
templateUrl: './sun-icon.html',
|
|
11
|
+
styleUrl: './sun-icon.css',
|
|
12
|
+
})
|
|
13
|
+
export class SunIcon {}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/* The sidebar is its own surface: `--color-muted` sits a step off `--color-background`
|
|
2
|
+
in both themes (near-white grey in light, a shade lighter than the base in dark). That
|
|
3
|
+
#f4f4f5-on-white step is subtle, so a 1px right border marks the at-rest sidebar | main
|
|
4
|
+
division: <kai-resizable>'s `line` handle is transparent at rest and only tints on
|
|
5
|
+
hover/drag, so the boundary has to come from the panel itself. The border uses
|
|
6
|
+
`var(--color-border)`, which now tracks the shell's light/dark tokens because the parent
|
|
7
|
+
<ResizableItem theme={theme}> forwards the theme (a light hairline in light mode, dark in
|
|
8
|
+
dark). `box-sizing: border-box` keeps the 1px inside the 280px item so it doesn't shift
|
|
9
|
+
the content width. Collapse is handled by the parent <ResizableItem collapsed> (the panel
|
|
10
|
+
+ its divider drop out), so the old `.sidebar.collapsed` rule is gone. */
|
|
11
|
+
.sidebar {
|
|
12
|
+
background: var(--color-muted);
|
|
13
|
+
border-right: 1px solid var(--color-border);
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
height: 100%;
|
|
16
|
+
min-height: 0;
|
|
17
|
+
overflow: hidden;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.sidebar > * {
|
|
21
|
+
display: block;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<aside class="sidebar">
|
|
2
|
+
<kai-conversations
|
|
3
|
+
[theme]="theme()"
|
|
4
|
+
[groups]="emptyGroups"
|
|
5
|
+
[conversations]="conversations()"
|
|
6
|
+
[attr.active-id]="activeId()"
|
|
7
|
+
[collapsed]="collapsed()"
|
|
8
|
+
(kai-conversation-select)="onSelect($event)"
|
|
9
|
+
(kai-new-chat)="newChat.emit()"
|
|
10
|
+
(kai-toggle-sidebar)="toggle.emit()"
|
|
11
|
+
></kai-conversations>
|
|
12
|
+
</aside>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA, Component, input, output } from '@angular/core';
|
|
2
|
+
import type { Theme } from '../../types';
|
|
3
|
+
import type { Conversation } from '../../../chat-data';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The conversation rail — a thin wrapper over `<kai-conversations>`. The `.sidebar` div
|
|
7
|
+
* owns the shell's right border (kept OFF the element so it follows the shell's
|
|
8
|
+
* light/dark tokens, not the light-mode ones the element pins on itself at `:host`).
|
|
9
|
+
* The rail's `collapsed` is CONTROLLED by the app's collapsed state, so it stays in
|
|
10
|
+
* sync with the parent `<kai-resizable-item collapsed>` and re-expands the list on
|
|
11
|
+
* restore; the internal hamburger still reports the toggle intent up via
|
|
12
|
+
* `(kai-toggle-sidebar)`.
|
|
13
|
+
*
|
|
14
|
+
* Array/object props (`groups`, `conversations`) are set as DOM PROPERTIES via
|
|
15
|
+
* Angular property binding (`[groups]`, `[conversations]`). `collapsed` is a boolean
|
|
16
|
+
* PROPERTY. `activeId` is a scalar the element reads as the `active-id` attribute, so
|
|
17
|
+
* it binds with `[attr.active-id]`. `emptyGroups` is a stable field, not an inline
|
|
18
|
+
* `[]` literal, so the property isn't re-set (and the list re-cleared) every change
|
|
19
|
+
* detection.
|
|
20
|
+
*/
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'app-sidebar',
|
|
23
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
24
|
+
templateUrl: './sidebar.html',
|
|
25
|
+
styleUrl: './sidebar.css',
|
|
26
|
+
})
|
|
27
|
+
export class Sidebar {
|
|
28
|
+
theme = input.required<Theme>();
|
|
29
|
+
conversations = input.required<Conversation[]>();
|
|
30
|
+
activeId = input.required<string>();
|
|
31
|
+
collapsed = input(false);
|
|
32
|
+
|
|
33
|
+
select = output<string>();
|
|
34
|
+
newChat = output<void>();
|
|
35
|
+
toggle = output<void>();
|
|
36
|
+
|
|
37
|
+
// Stable empty-groups reference (this demo uses a flat conversation list).
|
|
38
|
+
readonly emptyGroups: unknown[] = [];
|
|
39
|
+
|
|
40
|
+
onSelect(e: Event) {
|
|
41
|
+
this.select.emit((e as CustomEvent<{ id: string }>).detail.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<kai-button
|
|
2
|
+
[theme]="theme()"
|
|
3
|
+
variant="ghost"
|
|
4
|
+
size="icon"
|
|
5
|
+
label="Toggle light/dark theme"
|
|
6
|
+
(click)="toggle.emit()"
|
|
7
|
+
>
|
|
8
|
+
@if (theme() === 'light') {
|
|
9
|
+
<app-moon-icon slot="icon" aria-hidden="true"></app-moon-icon>
|
|
10
|
+
} @else {
|
|
11
|
+
<app-sun-icon slot="icon" aria-hidden="true"></app-sun-icon>
|
|
12
|
+
}
|
|
13
|
+
</kai-button>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA, Component, input, output } from '@angular/core';
|
|
2
|
+
import type { Theme } from '../../types';
|
|
3
|
+
import { MoonIcon } from '../icons/moon-icon/moon-icon';
|
|
4
|
+
import { SunIcon } from '../icons/sun-icon/sun-icon';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Light/dark switch for the top bar. Shows the moon in light mode (tap -> dark) and
|
|
8
|
+
* the sun in dark mode. `<kai-button>` is icon-only, so it carries the accessible
|
|
9
|
+
* `label`; the glyph is decorative (`aria-hidden`) in the `icon` slot. The native
|
|
10
|
+
* click on the shadow-root button bubbles (composed), so a plain `(click)` works.
|
|
11
|
+
*/
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'app-theme-toggle',
|
|
14
|
+
imports: [MoonIcon, SunIcon],
|
|
15
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
16
|
+
templateUrl: './theme-toggle.html',
|
|
17
|
+
})
|
|
18
|
+
export class ThemeToggle {
|
|
19
|
+
theme = input.required<Theme>();
|
|
20
|
+
toggle = output<void>();
|
|
21
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA, Component, computed, input } from '@angular/core';
|
|
2
|
+
import type { ChatMessage } from '@kitn.ai/ui';
|
|
3
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
4
|
+
import type { Theme } from '../../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The scrolling message list. `<kai-thread>` owns the message rendering, the
|
|
8
|
+
* centered fixed-width column, and stick-to-bottom scroll, so this component just
|
|
9
|
+
* bakes the per-message actions onto the assistant turns and wires the custom
|
|
10
|
+
* `speak` action to the browser's speech synthesis. `copy` (and the feedback votes)
|
|
11
|
+
* are handled inside the element.
|
|
12
|
+
*
|
|
13
|
+
* `messages` reaches the element as a DOM PROPERTY (`[messages]`); a NEW array
|
|
14
|
+
* reference per streaming chunk is what re-renders it. `withActions` is a computed
|
|
15
|
+
* that derives that fresh array whenever the `messages` signal input changes.
|
|
16
|
+
*/
|
|
17
|
+
@Component({
|
|
18
|
+
selector: 'app-thread-view',
|
|
19
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
20
|
+
templateUrl: './thread-view.html',
|
|
21
|
+
styleUrl: './thread-view.css',
|
|
22
|
+
})
|
|
23
|
+
export class ThreadView {
|
|
24
|
+
theme = input.required<Theme>();
|
|
25
|
+
messages = input.required<ChatMessage[]>();
|
|
26
|
+
|
|
27
|
+
// <kai-thread> reads `actions` off each message; only assistant replies get them.
|
|
28
|
+
withActions = computed<ChatMessage[]>(() =>
|
|
29
|
+
this.messages().map((m) =>
|
|
30
|
+
m.role === 'assistant'
|
|
31
|
+
? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
|
|
32
|
+
: m,
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
onMessageAction(e: Event) {
|
|
37
|
+
const detail = (e as CustomEvent<{ messageId: string; action: string }>).detail;
|
|
38
|
+
if (detail.action === 'speak') {
|
|
39
|
+
const m = this.messages().find((x) => x.id === detail.messageId);
|
|
40
|
+
if (!m) return;
|
|
41
|
+
window.speechSynthesis.cancel();
|
|
42
|
+
window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|