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.
Files changed (176) hide show
  1. package/README.md +311 -0
  2. package/dist/index.js +4433 -0
  3. package/dist/templates/angular/_gitignore +7 -0
  4. package/dist/templates/angular/angular.json +53 -0
  5. package/dist/templates/angular/package.json +30 -0
  6. package/dist/templates/angular/src/app/app.config.ts +11 -0
  7. package/dist/templates/angular/src/app/app.css +36 -0
  8. package/dist/templates/angular/src/app/app.html +51 -0
  9. package/dist/templates/angular/src/app/app.ts +78 -0
  10. package/dist/templates/angular/src/app/components/composer/composer.css +31 -0
  11. package/dist/templates/angular/src/app/components/composer/composer.html +18 -0
  12. package/dist/templates/angular/src/app/components/composer/composer.ts +81 -0
  13. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.css +3 -0
  14. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.html +12 -0
  15. package/dist/templates/angular/src/app/components/icons/moon-icon/moon-icon.ts +15 -0
  16. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.css +3 -0
  17. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.html +14 -0
  18. package/dist/templates/angular/src/app/components/icons/sun-icon/sun-icon.ts +13 -0
  19. package/dist/templates/angular/src/app/components/sidebar/sidebar.css +24 -0
  20. package/dist/templates/angular/src/app/components/sidebar/sidebar.html +12 -0
  21. package/dist/templates/angular/src/app/components/sidebar/sidebar.ts +43 -0
  22. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.html +13 -0
  23. package/dist/templates/angular/src/app/components/theme-toggle/theme-toggle.ts +21 -0
  24. package/dist/templates/angular/src/app/components/thread-view/thread-view.css +6 -0
  25. package/dist/templates/angular/src/app/components/thread-view/thread-view.html +6 -0
  26. package/dist/templates/angular/src/app/components/thread-view/thread-view.ts +45 -0
  27. package/dist/templates/angular/src/app/state/chat.store.ts +47 -0
  28. package/dist/templates/angular/src/app/state/conversations.store.ts +42 -0
  29. package/dist/templates/angular/src/app/state/voice-input.ts +57 -0
  30. package/dist/templates/angular/src/app/types.ts +2 -0
  31. package/dist/templates/angular/src/chat-data.ts +106 -0
  32. package/dist/templates/angular/src/index.html +16 -0
  33. package/dist/templates/angular/src/main.ts +16 -0
  34. package/dist/templates/angular/src/styles.css +10 -0
  35. package/dist/templates/angular/tsconfig.app.json +9 -0
  36. package/dist/templates/angular/tsconfig.json +22 -0
  37. package/dist/templates/nextjs/_gitignore +11 -0
  38. package/dist/templates/nextjs/app/chat-data.ts +125 -0
  39. package/dist/templates/nextjs/app/components/Composer.tsx +88 -0
  40. package/dist/templates/nextjs/app/components/HydrationBadge.tsx +68 -0
  41. package/dist/templates/nextjs/app/components/Sidebar.tsx +48 -0
  42. package/dist/templates/nextjs/app/components/ThemeToggle.tsx +27 -0
  43. package/dist/templates/nextjs/app/components/ThreadView.tsx +48 -0
  44. package/dist/templates/nextjs/app/components/icons/MoonIcon.tsx +25 -0
  45. package/dist/templates/nextjs/app/components/icons/SunIcon.tsx +25 -0
  46. package/dist/templates/nextjs/app/components/icons/index.ts +2 -0
  47. package/dist/templates/nextjs/app/globals.css +153 -0
  48. package/dist/templates/nextjs/app/hooks/index.ts +1 -0
  49. package/dist/templates/nextjs/app/hooks/useConversations.ts +42 -0
  50. package/dist/templates/nextjs/app/layout.tsx +50 -0
  51. package/dist/templates/nextjs/app/page.tsx +17 -0
  52. package/dist/templates/nextjs/app/theme.ts +10 -0
  53. package/dist/templates/nextjs/app/workspace.tsx +148 -0
  54. package/dist/templates/nextjs/next.config.mjs +22 -0
  55. package/dist/templates/nextjs/package.json +23 -0
  56. package/dist/templates/nextjs/postcss.config.mjs +16 -0
  57. package/dist/templates/nextjs/tsconfig.json +23 -0
  58. package/dist/templates/react/_gitignore +6 -0
  59. package/dist/templates/react/index.html +16 -0
  60. package/dist/templates/react/package.json +25 -0
  61. package/dist/templates/react/src/App.tsx +108 -0
  62. package/dist/templates/react/src/chat-data.ts +106 -0
  63. package/dist/templates/react/src/components/Composer.tsx +87 -0
  64. package/dist/templates/react/src/components/Sidebar.tsx +38 -0
  65. package/dist/templates/react/src/components/ThemeToggle.tsx +22 -0
  66. package/dist/templates/react/src/components/ThreadView.tsx +40 -0
  67. package/dist/templates/react/src/components/icons/MoonIcon.tsx +25 -0
  68. package/dist/templates/react/src/components/icons/SunIcon.tsx +25 -0
  69. package/dist/templates/react/src/components/icons/index.ts +2 -0
  70. package/dist/templates/react/src/hooks/index.ts +1 -0
  71. package/dist/templates/react/src/hooks/useConversations.ts +37 -0
  72. package/dist/templates/react/src/index.css +111 -0
  73. package/dist/templates/react/src/main.tsx +15 -0
  74. package/dist/templates/react/src/vite-env.d.ts +1 -0
  75. package/dist/templates/react/tsconfig.app.json +28 -0
  76. package/dist/templates/react/tsconfig.json +7 -0
  77. package/dist/templates/react/tsconfig.node.json +25 -0
  78. package/dist/templates/react/vite.config.ts +11 -0
  79. package/dist/templates/solid/_gitignore +6 -0
  80. package/dist/templates/solid/index.html +13 -0
  81. package/dist/templates/solid/package.json +23 -0
  82. package/dist/templates/solid/src/App.tsx +151 -0
  83. package/dist/templates/solid/src/chat-data.ts +234 -0
  84. package/dist/templates/solid/src/components/Composer.tsx +90 -0
  85. package/dist/templates/solid/src/components/Sidebar.tsx +36 -0
  86. package/dist/templates/solid/src/components/ThemeToggle.tsx +25 -0
  87. package/dist/templates/solid/src/components/ThreadView.tsx +122 -0
  88. package/dist/templates/solid/src/components/icons/index.tsx +84 -0
  89. package/dist/templates/solid/src/index.tsx +5 -0
  90. package/dist/templates/solid/src/lib/chat.ts +64 -0
  91. package/dist/templates/solid/src/lib/conversations.ts +54 -0
  92. package/dist/templates/solid/src/lib/types.ts +2 -0
  93. package/dist/templates/solid/src/styles.css +11 -0
  94. package/dist/templates/solid/tsconfig.json +18 -0
  95. package/dist/templates/solid/vite.config.ts +10 -0
  96. package/dist/templates/svelte/_gitignore +6 -0
  97. package/dist/templates/svelte/index.html +16 -0
  98. package/dist/templates/svelte/package.json +23 -0
  99. package/dist/templates/svelte/src/App.svelte +116 -0
  100. package/dist/templates/svelte/src/app.d.ts +20 -0
  101. package/dist/templates/svelte/src/chat-data.ts +106 -0
  102. package/dist/templates/svelte/src/components/Composer.svelte +102 -0
  103. package/dist/templates/svelte/src/components/Sidebar.svelte +61 -0
  104. package/dist/templates/svelte/src/components/ThemeToggle.svelte +45 -0
  105. package/dist/templates/svelte/src/components/ThreadView.svelte +49 -0
  106. package/dist/templates/svelte/src/components/icons/MoonIcon.svelte +24 -0
  107. package/dist/templates/svelte/src/components/icons/SunIcon.svelte +24 -0
  108. package/dist/templates/svelte/src/index.css +111 -0
  109. package/dist/templates/svelte/src/lib/chat.svelte.ts +52 -0
  110. package/dist/templates/svelte/src/lib/conversations.svelte.ts +46 -0
  111. package/dist/templates/svelte/src/lib/types.ts +2 -0
  112. package/dist/templates/svelte/src/lib/voiceInput.ts +57 -0
  113. package/dist/templates/svelte/src/main.ts +18 -0
  114. package/dist/templates/svelte/svelte.config.js +8 -0
  115. package/dist/templates/svelte/tsconfig.app.json +28 -0
  116. package/dist/templates/svelte/tsconfig.json +7 -0
  117. package/dist/templates/svelte/tsconfig.node.json +25 -0
  118. package/dist/templates/svelte/vite.config.ts +15 -0
  119. package/dist/templates/tanstack-start/_gitignore +14 -0
  120. package/dist/templates/tanstack-start/package.json +28 -0
  121. package/dist/templates/tanstack-start/serve.mjs +129 -0
  122. package/dist/templates/tanstack-start/serve.traversal.test.mjs +445 -0
  123. package/dist/templates/tanstack-start/src/chat-data.ts +124 -0
  124. package/dist/templates/tanstack-start/src/components/Composer.tsx +87 -0
  125. package/dist/templates/tanstack-start/src/components/HydrationBadge.tsx +63 -0
  126. package/dist/templates/tanstack-start/src/components/Sidebar.tsx +43 -0
  127. package/dist/templates/tanstack-start/src/components/ThemeToggle.tsx +26 -0
  128. package/dist/templates/tanstack-start/src/components/ThreadView.tsx +48 -0
  129. package/dist/templates/tanstack-start/src/components/icons/MoonIcon.tsx +25 -0
  130. package/dist/templates/tanstack-start/src/components/icons/SunIcon.tsx +25 -0
  131. package/dist/templates/tanstack-start/src/components/icons/index.ts +2 -0
  132. package/dist/templates/tanstack-start/src/hooks/index.ts +1 -0
  133. package/dist/templates/tanstack-start/src/hooks/useConversations.ts +42 -0
  134. package/dist/templates/tanstack-start/src/router.tsx +16 -0
  135. package/dist/templates/tanstack-start/src/routes/__root.tsx +62 -0
  136. package/dist/templates/tanstack-start/src/routes/index.tsx +135 -0
  137. package/dist/templates/tanstack-start/src/styles.css +153 -0
  138. package/dist/templates/tanstack-start/src/theme.ts +10 -0
  139. package/dist/templates/tanstack-start/tsconfig.json +16 -0
  140. package/dist/templates/tanstack-start/vite.config.ts +28 -0
  141. package/dist/templates/vanilla/_gitignore +6 -0
  142. package/dist/templates/vanilla/index.html +16 -0
  143. package/dist/templates/vanilla/package.json +19 -0
  144. package/dist/templates/vanilla/src/chat-data.ts +106 -0
  145. package/dist/templates/vanilla/src/index.css +111 -0
  146. package/dist/templates/vanilla/src/main.ts +53 -0
  147. package/dist/templates/vanilla/src/state.ts +119 -0
  148. package/dist/templates/vanilla/src/view.ts +222 -0
  149. package/dist/templates/vanilla/src/vite-env.d.ts +1 -0
  150. package/dist/templates/vanilla/src/voice-input.ts +55 -0
  151. package/dist/templates/vanilla/tsconfig.json +24 -0
  152. package/dist/templates/vanilla/vite.config.ts +14 -0
  153. package/dist/templates/vue/_gitignore +6 -0
  154. package/dist/templates/vue/index.html +16 -0
  155. package/dist/templates/vue/package.json +23 -0
  156. package/dist/templates/vue/src/App.vue +104 -0
  157. package/dist/templates/vue/src/chat-data.ts +106 -0
  158. package/dist/templates/vue/src/components/Composer.vue +96 -0
  159. package/dist/templates/vue/src/components/Sidebar.vue +48 -0
  160. package/dist/templates/vue/src/components/ThemeToggle.vue +27 -0
  161. package/dist/templates/vue/src/components/ThreadView.vue +50 -0
  162. package/dist/templates/vue/src/components/icons/MoonIcon.vue +24 -0
  163. package/dist/templates/vue/src/components/icons/SunIcon.vue +24 -0
  164. package/dist/templates/vue/src/composables/index.ts +3 -0
  165. package/dist/templates/vue/src/composables/useChat.ts +47 -0
  166. package/dist/templates/vue/src/composables/useConversations.ts +42 -0
  167. package/dist/templates/vue/src/composables/useVoiceInput.ts +57 -0
  168. package/dist/templates/vue/src/index.css +111 -0
  169. package/dist/templates/vue/src/main.ts +18 -0
  170. package/dist/templates/vue/src/types.ts +4 -0
  171. package/dist/templates/vue/src/vite-env.d.ts +1 -0
  172. package/dist/templates/vue/tsconfig.app.json +29 -0
  173. package/dist/templates/vue/tsconfig.json +7 -0
  174. package/dist/templates/vue/tsconfig.node.json +25 -0
  175. package/dist/templates/vue/vite.config.ts +24 -0
  176. package/package.json +63 -0
@@ -0,0 +1,7 @@
1
+ node_modules/
2
+ dist/
3
+ .angular/
4
+ .env
5
+ .env.local
6
+ *.log
7
+ .DS_Store
@@ -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">&#64;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>&#64;</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,3 @@
1
+ :host {
2
+ display: inline-flex;
3
+ }
@@ -0,0 +1,12 @@
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
+ <path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
12
+ </svg>
@@ -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,3 @@
1
+ :host {
2
+ display: inline-flex;
3
+ }
@@ -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,6 @@
1
+ /* <kai-thread> scrolls internally and centers its own fixed-width column, so the
2
+ host just needs to fill the remaining height of the main flex column. */
3
+ .thread {
4
+ flex: 1;
5
+ min-height: 0;
6
+ }
@@ -0,0 +1,6 @@
1
+ <kai-thread
2
+ class="thread"
3
+ [theme]="theme()"
4
+ [messages]="withActions()"
5
+ (kai-message-action)="onMessageAction($event)"
6
+ ></kai-thread>
@@ -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
+ }