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,96 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref } from 'vue';
|
|
3
|
+
import type { KaiPromptInputElementProps } from '@kitn.ai/ui/elements';
|
|
4
|
+
import type { Theme } from '../types';
|
|
5
|
+
import { useVoiceInput } from '../composables';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The bottom composer. Wraps `<kai-prompt-input>` and owns the input-DOM concerns:
|
|
9
|
+
* the element ref, a live text mirror, clear-on-submit, and seeding voice.
|
|
10
|
+
*
|
|
11
|
+
* The prompt input stays UNCONTROLLED so the `/`+`@` trigger menus keep a live
|
|
12
|
+
* caret. We NEVER bind `:value` with a plain STRING: that flips the element into
|
|
13
|
+
* controlled mode, which re-applies the property and collapses the shadow-DOM
|
|
14
|
+
* selection, disabling the caret-anchored menu. So clear-on-submit goes through
|
|
15
|
+
* the element's `clear()` method, and voice seeds a ComposerDoc (a non-string
|
|
16
|
+
* value is a one-time seed that leaves the input uncontrolled). We keep the live
|
|
17
|
+
* text mirrored in `liveText` (from `kai-value-change`) so voice can append.
|
|
18
|
+
*/
|
|
19
|
+
defineProps<{
|
|
20
|
+
theme: Theme;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
suggestions: string[];
|
|
23
|
+
// The kit owns this shape. Take it straight off the element's own prop type
|
|
24
|
+
// (`@kitn.ai/ui/elements`) instead of re-declaring it here, so a change to the
|
|
25
|
+
// trigger contract shows up as a build error rather than silent drift.
|
|
26
|
+
triggers: NonNullable<KaiPromptInputElementProps['triggers']>;
|
|
27
|
+
}>();
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
submit: [value: string];
|
|
31
|
+
suggestion: [value: string];
|
|
32
|
+
}>();
|
|
33
|
+
|
|
34
|
+
// Ref to the underlying <kai-prompt-input> element for the imperative pushes
|
|
35
|
+
// (clear() on submit, ComposerDoc seed from voice); `liveText` mirrors the text.
|
|
36
|
+
const promptRef = ref<HTMLElement | null>(null);
|
|
37
|
+
let liveText = '';
|
|
38
|
+
|
|
39
|
+
// Voice: on a transcript, append it to the current text and seed the (uncontrolled)
|
|
40
|
+
// input as a ComposerDoc. A non-string `value` is a one-time seed into the
|
|
41
|
+
// element's internal state; a plain STRING would instead flip it into controlled
|
|
42
|
+
// mode and re-break the `/`+`@` triggers and submit.
|
|
43
|
+
const { supported: voiceSupported, start: startVoice } = useVoiceInput((transcript) => {
|
|
44
|
+
const next = (liveText ? liveText + ' ' : '') + transcript;
|
|
45
|
+
liveText = next;
|
|
46
|
+
const el = promptRef.value as (HTMLElement & { value?: unknown }) | null;
|
|
47
|
+
if (el) el.value = [{ type: 'text', text: next }];
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function handleVoice() {
|
|
51
|
+
// Mic is Chromium-only; degrade gracefully everywhere else.
|
|
52
|
+
if (!voiceSupported) {
|
|
53
|
+
alert('Voice input needs a Chromium browser.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
startVoice();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function onValueChange(e: Event) {
|
|
60
|
+
liveText = (e as CustomEvent<{ value: string }>).detail.value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onSubmit(e: Event) {
|
|
64
|
+
// Clear our own uncontrolled input first — reset the mirror + call the element's
|
|
65
|
+
// clear() method (uncontrolled-safe; fires kai-value-change internally, never a
|
|
66
|
+
// string assignment) — then hand the text up. The parent owns append + streaming.
|
|
67
|
+
liveText = '';
|
|
68
|
+
(promptRef.value as (HTMLElement & { clear?: () => void }) | null)?.clear?.();
|
|
69
|
+
emit('submit', (e as CustomEvent<{ value: string }>).detail.value);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onSuggestionClick(e: Event) {
|
|
73
|
+
emit('suggestion', (e as CustomEvent<{ value: string }>).detail.value);
|
|
74
|
+
}
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div class="composer">
|
|
79
|
+
<kai-prompt-input
|
|
80
|
+
ref="promptRef"
|
|
81
|
+
:theme="theme"
|
|
82
|
+
placeholder="Message the demo…"
|
|
83
|
+
:loading="loading"
|
|
84
|
+
:suggestions.prop="suggestions"
|
|
85
|
+
:triggers.prop="triggers"
|
|
86
|
+
:voice.prop="true"
|
|
87
|
+
@kai-voice="handleVoice"
|
|
88
|
+
@kai-value-change="onValueChange"
|
|
89
|
+
@kai-submit="onSubmit"
|
|
90
|
+
@kai-suggestion-click="onSuggestionClick"
|
|
91
|
+
></kai-prompt-input>
|
|
92
|
+
<p class="composer-hint">
|
|
93
|
+
Type <kbd>/</kbd> for skills · <kbd>@</kbd> for agents
|
|
94
|
+
</p>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
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`) MUST be set as DOM properties, so
|
|
15
|
+
* they use the `.prop` modifier. `activeId` / `collapsed` are scalars.
|
|
16
|
+
*/
|
|
17
|
+
defineProps<{
|
|
18
|
+
theme: Theme;
|
|
19
|
+
conversations: Conversation[];
|
|
20
|
+
activeId: string;
|
|
21
|
+
collapsed: boolean;
|
|
22
|
+
}>();
|
|
23
|
+
|
|
24
|
+
const emit = defineEmits<{
|
|
25
|
+
select: [id: string];
|
|
26
|
+
newChat: [];
|
|
27
|
+
toggle: [];
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
function onSelect(e: Event) {
|
|
31
|
+
emit('select', (e as CustomEvent<{ id: string }>).detail.id);
|
|
32
|
+
}
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<template>
|
|
36
|
+
<aside class="sidebar">
|
|
37
|
+
<kai-conversations
|
|
38
|
+
:theme="theme"
|
|
39
|
+
:groups.prop="[]"
|
|
40
|
+
:conversations.prop="conversations"
|
|
41
|
+
:active-id="activeId"
|
|
42
|
+
:collapsed.prop="collapsed"
|
|
43
|
+
@kai-conversation-select="onSelect"
|
|
44
|
+
@kai-new-chat="emit('newChat')"
|
|
45
|
+
@kai-toggle-sidebar="emit('toggle')"
|
|
46
|
+
></kai-conversations>
|
|
47
|
+
</aside>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Theme } from '../types';
|
|
3
|
+
import MoonIcon from './icons/MoonIcon.vue';
|
|
4
|
+
import SunIcon from './icons/SunIcon.vue';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Light/dark switch for the top bar. Shows the moon in light mode (tap -> dark)
|
|
8
|
+
* and the sun in dark mode. `<kai-button>` is icon-only, so it carries the
|
|
9
|
+
* accessible label; the glyph is decorative (`aria-hidden`) in the `icon` slot.
|
|
10
|
+
* The native click on the shadow-root button bubbles, so a plain `@click` works.
|
|
11
|
+
*/
|
|
12
|
+
defineProps<{ theme: Theme }>();
|
|
13
|
+
defineEmits<{ toggle: [] }>();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<kai-button
|
|
18
|
+
:theme="theme"
|
|
19
|
+
variant="ghost"
|
|
20
|
+
size="icon"
|
|
21
|
+
label="Toggle light/dark theme"
|
|
22
|
+
@click="$emit('toggle')"
|
|
23
|
+
>
|
|
24
|
+
<MoonIcon v-if="theme === 'light'" slot="icon" aria-hidden="true" />
|
|
25
|
+
<SunIcon v-else slot="icon" aria-hidden="true" />
|
|
26
|
+
</kai-button>
|
|
27
|
+
</template>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
import type { ChatMessage } from '@kitn.ai/ui';
|
|
4
|
+
import { partsToText } from '@kitn.ai/ui/state';
|
|
5
|
+
import type { Theme } from '../types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The scrolling message list. `<kai-thread>` owns the message rendering, the
|
|
9
|
+
* centered fixed-width column, and stick-to-bottom scroll, so this component just
|
|
10
|
+
* bakes the per-message actions onto the assistant turns and wires the custom
|
|
11
|
+
* `speak` action to the browser's speech synthesis. `copy` (and the feedback
|
|
12
|
+
* votes) are handled inside the element.
|
|
13
|
+
*
|
|
14
|
+
* `messages` is passed to the element as a DOM PROPERTY (`.prop`); a NEW array
|
|
15
|
+
* reference per streaming chunk is what re-renders it. The `withActions` computed
|
|
16
|
+
* derives that fresh array whenever `messages` changes.
|
|
17
|
+
*/
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
theme: Theme;
|
|
20
|
+
messages: ChatMessage[];
|
|
21
|
+
}>();
|
|
22
|
+
|
|
23
|
+
// <kai-thread> reads `actions` off each message; only assistant replies get them.
|
|
24
|
+
const withActions = computed<ChatMessage[]>(() =>
|
|
25
|
+
props.messages.map((m) =>
|
|
26
|
+
m.role === 'assistant'
|
|
27
|
+
? { ...m, actions: ['copy', { id: 'speak', label: 'Read aloud', icon: 'volume-2' }] }
|
|
28
|
+
: m,
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
function onMessageAction(e: Event) {
|
|
33
|
+
const detail = (e as CustomEvent<{ messageId: string; action: string }>).detail;
|
|
34
|
+
if (detail.action === 'speak') {
|
|
35
|
+
const m = props.messages.find((x) => x.id === detail.messageId);
|
|
36
|
+
if (!m) return;
|
|
37
|
+
window.speechSynthesis.cancel();
|
|
38
|
+
window.speechSynthesis.speak(new SpeechSynthesisUtterance(partsToText(m.parts)));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<kai-thread
|
|
45
|
+
class="thread"
|
|
46
|
+
:theme="theme"
|
|
47
|
+
:messages.prop="withActions"
|
|
48
|
+
@kai-message-action="onMessageAction"
|
|
49
|
+
></kai-thread>
|
|
50
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Moon glyph — shown in light mode (tap the toggle -> dark). moon/sun aren't in
|
|
4
|
+
* the kit's icon set, so the example owns them. Fallthrough attributes land on the
|
|
5
|
+
* root <svg>, so `slot="icon"` and `aria-hidden` pass through and it drops into
|
|
6
|
+
* <kai-button>'s icon slot.
|
|
7
|
+
*/
|
|
8
|
+
withDefaults(defineProps<{ size?: number }>(), { size: 20 });
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<svg
|
|
13
|
+
:width="size"
|
|
14
|
+
:height="size"
|
|
15
|
+
viewBox="0 0 24 24"
|
|
16
|
+
fill="none"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
stroke-width="2"
|
|
19
|
+
stroke-linecap="round"
|
|
20
|
+
stroke-linejoin="round"
|
|
21
|
+
>
|
|
22
|
+
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
|
23
|
+
</svg>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Sun glyph — shown in dark mode (tap the toggle -> light). See MoonIcon for why
|
|
4
|
+
* the example owns these icons. Fallthrough attributes land on the root <svg>.
|
|
5
|
+
*/
|
|
6
|
+
withDefaults(defineProps<{ size?: number }>(), { size: 20 });
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<svg
|
|
11
|
+
:width="size"
|
|
12
|
+
:height="size"
|
|
13
|
+
viewBox="0 0 24 24"
|
|
14
|
+
fill="none"
|
|
15
|
+
stroke="currentColor"
|
|
16
|
+
stroke-width="2"
|
|
17
|
+
stroke-linecap="round"
|
|
18
|
+
stroke-linejoin="round"
|
|
19
|
+
>
|
|
20
|
+
<circle cx="12" cy="12" r="4" />
|
|
21
|
+
<path d="M12 2v2" /><path d="M12 20v2" /><path d="m4.93 4.93 1.41 1.41" /><path d="m17.66 17.66 1.41 1.41" />
|
|
22
|
+
<path d="M2 12h2" /><path d="M20 12h2" /><path d="m6.34 17.66-1.41 1.41" /><path d="m19.07 4.93-1.41 1.41" />
|
|
23
|
+
</svg>
|
|
24
|
+
</template>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ref, shallowRef, type Ref } from 'vue';
|
|
2
|
+
import type { ChatMessage } from '@kitn.ai/ui';
|
|
3
|
+
import {
|
|
4
|
+
appendMessage,
|
|
5
|
+
createAssistantStream,
|
|
6
|
+
onStreamSettled,
|
|
7
|
+
type SetMessages,
|
|
8
|
+
type AssistantStream,
|
|
9
|
+
} from '@kitn.ai/ui/state';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Vue port of the kit's `useKaiChat` (a React hook), built on the SAME
|
|
13
|
+
* framework-neutral state core (`@kitn.ai/ui/state`) the React hook uses. It owns
|
|
14
|
+
* the message array + the `loading` flag and exposes ergonomic ops. Swap
|
|
15
|
+
* `mockResponse(text)` for a real `fetch` to ship a real app.
|
|
16
|
+
*
|
|
17
|
+
* `messages` is a `shallowRef` on purpose: every op assigns a BRAND-NEW array
|
|
18
|
+
* reference (`messages.value = updater(prev)`), which is exactly what re-renders
|
|
19
|
+
* `<kai-thread>` — mutating the array in place does not. A shallowRef also keeps
|
|
20
|
+
* the array plain (no deep proxy), so what reaches the element's `messages`
|
|
21
|
+
* property is a clean `ChatMessage[]`.
|
|
22
|
+
*/
|
|
23
|
+
export interface ChatController {
|
|
24
|
+
messages: Ref<ChatMessage[]>;
|
|
25
|
+
loading: Ref<boolean>;
|
|
26
|
+
append: (msg: ChatMessage) => void;
|
|
27
|
+
setMessages: (next: ChatMessage[]) => void;
|
|
28
|
+
streamAssistant: (init?: Partial<ChatMessage>) => AssistantStream;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function useChat(initialMessages: ChatMessage[] = []): ChatController {
|
|
32
|
+
const messages = shallowRef<ChatMessage[]>([...initialMessages]);
|
|
33
|
+
const loading = ref(false);
|
|
34
|
+
|
|
35
|
+
// The one universal contract the state core drives: a functional-updater setter
|
|
36
|
+
// (React setState shape) that assigns a fresh array each time.
|
|
37
|
+
const set: SetMessages = (updater) => { messages.value = updater(messages.value); };
|
|
38
|
+
|
|
39
|
+
const append = (msg: ChatMessage) => set((prev) => appendMessage(prev, msg));
|
|
40
|
+
const setMessages = (next: ChatMessage[]) => { messages.value = [...next]; };
|
|
41
|
+
const streamAssistant = (init?: Partial<ChatMessage>): AssistantStream => {
|
|
42
|
+
loading.value = true;
|
|
43
|
+
return onStreamSettled(createAssistantStream(set, init), () => { loading.value = false; });
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
return { messages, loading, append, setMessages, streamAssistant };
|
|
47
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { shallowRef, type Ref } from 'vue';
|
|
2
|
+
import { THREADS, newId, type Conversation } from '../chat-data';
|
|
3
|
+
import type { ChatController } from './useChat';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Owns the conversation list + which one is active, plus the in-memory `THREADS`
|
|
7
|
+
* stash: on select/new we swap the OPEN thread out to `THREADS` and swap the
|
|
8
|
+
* picked one in. Takes the `useChat` controller so it can read the live messages
|
|
9
|
+
* when stashing and reset them when loading another thread. Mirrors the React
|
|
10
|
+
* `useConversations` hook.
|
|
11
|
+
*/
|
|
12
|
+
export interface ConversationsController {
|
|
13
|
+
conversations: Ref<Conversation[]>;
|
|
14
|
+
activeId: Ref<string>;
|
|
15
|
+
selectConversation: (id: string) => void;
|
|
16
|
+
newChat: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useConversations(chat: ChatController, initial: Conversation[]): ConversationsController {
|
|
20
|
+
const conversations = shallowRef<Conversation[]>([...initial]);
|
|
21
|
+
const activeId = shallowRef(initial[0].id);
|
|
22
|
+
|
|
23
|
+
const selectConversation = (id: string) => {
|
|
24
|
+
THREADS[activeId.value] = chat.messages.value; // stash the open thread
|
|
25
|
+
activeId.value = id;
|
|
26
|
+
chat.setMessages(THREADS[id] ?? []); // load the picked one
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const newChat = () => {
|
|
30
|
+
const id = newId();
|
|
31
|
+
THREADS[activeId.value] = chat.messages.value;
|
|
32
|
+
THREADS[id] = [];
|
|
33
|
+
conversations.value = [
|
|
34
|
+
{ id, title: 'New conversation', scope: { type: 'collection' }, messageCount: 0, lastMessageAt: new Date().toISOString(), updatedAt: new Date().toISOString() },
|
|
35
|
+
...conversations.value,
|
|
36
|
+
];
|
|
37
|
+
activeId.value = id;
|
|
38
|
+
chat.setMessages([]);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return { conversations, activeId, selectConversation, newChat };
|
|
42
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Minimal shape of a Web Speech recognition result event — enough to read the
|
|
2
|
+
* first transcript. The full SpeechRecognition API isn't in TS's lib.dom yet. */
|
|
3
|
+
interface SpeechResultEvent {
|
|
4
|
+
results: ArrayLike<ArrayLike<{ transcript: string }>>;
|
|
5
|
+
}
|
|
6
|
+
interface SpeechRecognitionLike {
|
|
7
|
+
lang: string;
|
|
8
|
+
interimResults: boolean;
|
|
9
|
+
onresult: ((event: SpeechResultEvent) => void) | null;
|
|
10
|
+
start(): void;
|
|
11
|
+
}
|
|
12
|
+
type SpeechRecognitionCtor = new () => SpeechRecognitionLike;
|
|
13
|
+
|
|
14
|
+
/** Resolve the Chromium (`webkitSpeechRecognition`) or standard constructor, if any. */
|
|
15
|
+
function getSpeechRecognition(): SpeechRecognitionCtor | undefined {
|
|
16
|
+
if (typeof window === 'undefined') return undefined;
|
|
17
|
+
const w = window as unknown as {
|
|
18
|
+
SpeechRecognition?: SpeechRecognitionCtor;
|
|
19
|
+
webkitSpeechRecognition?: SpeechRecognitionCtor;
|
|
20
|
+
};
|
|
21
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Web Speech mic → one-shot transcript. Chromium-only (`webkitSpeechRecognition`);
|
|
26
|
+
* `supported` is false elsewhere so the caller can degrade gracefully. Vue port of
|
|
27
|
+
* the kit's `useVoiceInput` (a React hook). It hands the raw transcript to
|
|
28
|
+
* `onTranscript` and lets the caller decide what to do with it (the composer mic
|
|
29
|
+
* appends it to the live text). No composer or DOM knowledge.
|
|
30
|
+
*
|
|
31
|
+
* @param onTranscript called with the final recognized transcript.
|
|
32
|
+
* @returns `supported` (Chromium-only availability) and a `start` that begins one
|
|
33
|
+
* recognition pass (a no-op when unsupported).
|
|
34
|
+
*/
|
|
35
|
+
export function useVoiceInput(onTranscript: (transcript: string) => void): {
|
|
36
|
+
supported: boolean;
|
|
37
|
+
start: () => void;
|
|
38
|
+
} {
|
|
39
|
+
// Latest-callback ref: keeps `start` stable while always calling the current
|
|
40
|
+
// `onTranscript`, mirroring the React hook (no stale closure).
|
|
41
|
+
const cbRef = { current: onTranscript };
|
|
42
|
+
cbRef.current = onTranscript;
|
|
43
|
+
|
|
44
|
+
const supported = getSpeechRecognition() !== undefined;
|
|
45
|
+
|
|
46
|
+
const start = () => {
|
|
47
|
+
const Recognition = getSpeechRecognition();
|
|
48
|
+
if (!Recognition) return;
|
|
49
|
+
const rec = new Recognition();
|
|
50
|
+
rec.lang = 'en-US';
|
|
51
|
+
rec.interimResults = false;
|
|
52
|
+
rec.onresult = (event) => cbRef.current(event.results[0][0].transcript);
|
|
53
|
+
rec.start();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return { supported, start };
|
|
57
|
+
}
|
|
@@ -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,18 @@
|
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import '@kitn.ai/ui/elements'; // registers the kai-* custom elements (async)
|
|
3
|
+
import '@kitn.ai/ui/theme.tokens.css'; // plain --color-* tokens for the shell (NOT the tailwind source)
|
|
4
|
+
import './index.css';
|
|
5
|
+
import App from './App.vue';
|
|
6
|
+
|
|
7
|
+
const root = document.getElementById('app');
|
|
8
|
+
if (!root) throw new Error('Root element not found');
|
|
9
|
+
|
|
10
|
+
// The kai-* elements register asynchronously. Wait until the tags we use are
|
|
11
|
+
// defined before mounting, so Vue's `:prop.prop` bindings land on UPGRADED
|
|
12
|
+
// elements. Raw web-component consumers have no upgrade-race guard (unlike the
|
|
13
|
+
// React wrappers), so a property set before upgrade is lost and static seed data
|
|
14
|
+
// never re-applies. Attributes survive upgrade; array/object properties do not.
|
|
15
|
+
const TAGS = ['kai-resizable', 'kai-resizable-item', 'kai-conversations', 'kai-thread', 'kai-prompt-input', 'kai-button'];
|
|
16
|
+
Promise.all(TAGS.map((t) => customElements.whenDefined(t))).then(() => {
|
|
17
|
+
createApp(App).mount(root);
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
/* Vue SFC JSX handling (harmless with no .tsx present) */
|
|
18
|
+
"jsx": "preserve",
|
|
19
|
+
"jsxImportSource": "vue",
|
|
20
|
+
|
|
21
|
+
/* Linting */
|
|
22
|
+
"strict": true,
|
|
23
|
+
"noUnusedLocals": true,
|
|
24
|
+
"noUnusedParameters": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": true,
|
|
26
|
+
"noUncheckedSideEffectImports": true
|
|
27
|
+
},
|
|
28
|
+
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]
|
|
29
|
+
}
|
|
@@ -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,24 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import vue from '@vitejs/plugin-vue';
|
|
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, no source stubs. Build the kit first (`nx build ui`).
|
|
7
|
+
//
|
|
8
|
+
// The one Vue-specific bit: tell the template compiler that every `kai-*` tag is a
|
|
9
|
+
// native custom element. Without this Vue tries to resolve them as Vue components
|
|
10
|
+
// (warning "unknown custom element"); with it, Vue leaves them alone and passes
|
|
11
|
+
// props/events straight through to the DOM.
|
|
12
|
+
// https://vite.dev/config/
|
|
13
|
+
export default defineConfig({
|
|
14
|
+
plugins: [
|
|
15
|
+
vue({
|
|
16
|
+
template: {
|
|
17
|
+
compilerOptions: {
|
|
18
|
+
isCustomElement: (tag) => tag.startsWith('kai-'),
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
server: { port: 5174 },
|
|
24
|
+
});
|