@kitnai/chat 0.3.1 → 0.5.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 +35 -5
- package/dist/custom-elements.json +2969 -0
- package/dist/kitn-chat.es.js +52 -39
- package/dist/llms/llms-full.txt +718 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/theme.tokens.css +137 -0
- package/frameworks/react/index.tsx +584 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +718 -0
- package/llms.txt +104 -0
- package/package.json +53 -6
- package/src/components/attachments.tsx +4 -2
- package/src/components/chain-of-thought.tsx +1 -1
- package/src/components/chat-scope-picker.tsx +2 -2
- package/src/components/chat-thread.tsx +217 -0
- package/src/components/checkpoint.tsx +7 -3
- package/src/components/context.tsx +14 -18
- package/src/components/conversation-item.tsx +1 -1
- package/src/components/conversation-list.tsx +5 -4
- package/src/components/message-skills.tsx +1 -1
- package/src/components/message.tsx +1 -0
- package/src/components/model-switcher.tsx +3 -3
- package/src/components/prompt-input.tsx +20 -2
- package/src/components/reasoning.tsx +2 -2
- package/src/components/scroll-button.tsx +1 -0
- package/src/components/slash-command.tsx +17 -8
- package/src/components/source.tsx +2 -2
- package/src/components/thinking-bar.tsx +2 -2
- package/src/components/tool.tsx +17 -6
- package/src/components/voice-input.tsx +5 -1
- package/src/elements/attachments.tsx +132 -0
- package/src/elements/chain-of-thought.tsx +45 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-workspace.tsx +122 -0
- package/src/elements/chat.tsx +31 -228
- package/src/elements/checkpoint.tsx +43 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +1 -1
- package/src/elements/context-meter.tsx +71 -0
- package/src/elements/conversation-list.tsx +6 -0
- package/src/elements/default-input.tsx +22 -1
- package/src/elements/define.tsx +98 -12
- package/src/elements/element-types.d.ts +444 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.tsx +33 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/kitn-attachments.stories.tsx +181 -0
- package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
- package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
- package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
- package/src/elements/kitn-checkpoint.stories.tsx +71 -0
- package/src/elements/kitn-code-block.stories.tsx +82 -0
- package/src/elements/kitn-context-meter.stories.tsx +85 -0
- package/src/elements/kitn-empty.stories.tsx +110 -0
- package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
- package/src/elements/kitn-file-upload.stories.tsx +81 -0
- package/src/elements/kitn-image.stories.tsx +70 -0
- package/src/elements/kitn-loader.stories.tsx +87 -0
- package/src/elements/kitn-markdown.stories.tsx +75 -0
- package/src/elements/kitn-message-skills.stories.tsx +74 -0
- package/src/elements/kitn-message.stories.tsx +105 -0
- package/src/elements/kitn-model-switcher.stories.tsx +80 -0
- package/src/elements/kitn-prompt-input.stories.tsx +74 -16
- package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
- package/src/elements/kitn-reasoning.stories.tsx +76 -0
- package/src/elements/kitn-response-stream.stories.tsx +79 -0
- package/src/elements/kitn-source-list.stories.tsx +77 -0
- package/src/elements/kitn-source.stories.tsx +87 -0
- package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
- package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
- package/src/elements/kitn-tool.stories.tsx +88 -0
- package/src/elements/kitn-voice-input.stories.tsx +87 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.tsx +22 -0
- package/src/elements/message.tsx +125 -0
- package/src/elements/model-switcher.tsx +35 -0
- package/src/elements/prompt-input.tsx +83 -7
- package/src/elements/prompt-suggestions.tsx +58 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +32 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/source.tsx +67 -0
- package/src/elements/styles.css +14 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +0 -1
- package/src/primitives/chat-config.tsx +3 -3
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ForAIAgents.mdx +93 -0
- package/src/stories/docs/GettingStarted.mdx +2 -2
- package/src/stories/docs/Installation.mdx +29 -2
- package/src/stories/docs/Integrations.mdx +417 -15
- package/src/stories/docs/Introduction.mdx +17 -8
- package/src/stories/docs/Theming.mdx +1 -1
- package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
- package/src/stories/pattern-docked-widget.stories.tsx +93 -0
- package/src/stories/pattern-empty-state.stories.tsx +76 -0
- package/src/stories/typography.stories.tsx +78 -0
- package/src/ui/button.tsx +1 -1
- package/src/ui/collapsible.stories.tsx +70 -0
- package/src/ui/collapsible.tsx +119 -8
- package/src/ui/dropdown.stories.tsx +60 -0
- package/src/ui/dropdown.tsx +177 -12
- package/src/ui/hover-card.stories.tsx +78 -0
- package/src/ui/hover-card.tsx +147 -26
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +151 -0
- package/src/ui/scroll-area.stories.tsx +51 -0
- package/src/ui/textarea.stories.tsx +77 -0
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/tooltip.stories.tsx +1 -1
- package/src/ui/tooltip.tsx +59 -13
- package/src/utils/cn.ts +19 -1
- package/theme.css +76 -43
- package/src/ui/dialog.tsx +0 -21
package/llms.txt
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<!-- AUTO-GENERATED by scripts/gen-llms.mjs — do not edit by hand. Run `npm run build`. -->
|
|
2
|
+
# @kitnai/chat
|
|
3
|
+
|
|
4
|
+
> Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. 28 `kitn-*` custom elements: streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; the SolidJS runtime it is authored in is bundled in, so the host needs nothing.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @kitnai/chat
|
|
10
|
+
# SolidJS consumers also need the peer dep:
|
|
11
|
+
npm install solid-js
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## #1 rule: array/object data goes on JS PROPERTIES, not HTML attributes
|
|
15
|
+
|
|
16
|
+
This is the single most common mistake. Arrays and objects (`messages`, `models`, `context`, `suggestions`, `slashCommands`, …) MUST be assigned as JavaScript properties on the element. They CANNOT be passed as HTML attributes — an HTML attribute is always a string and will be ignored or mis-parsed.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
const chat = document.querySelector('kitn-chat');
|
|
20
|
+
chat.messages = [{ id: '1', role: 'assistant', content: 'Hi!' }]; // ✅ property
|
|
21
|
+
```
|
|
22
|
+
```html
|
|
23
|
+
<kitn-chat messages="[...]"></kitn-chat> <!-- ❌ never works -->
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Only scalar values (string/number/boolean) work as attributes (e.g. `placeholder`, `loading`, `theme`).
|
|
27
|
+
|
|
28
|
+
## Two layers
|
|
29
|
+
|
|
30
|
+
**Layer 1 — batteries-included web components** (`import '@kitnai/chat/elements'`):
|
|
31
|
+
Drop an element into any framework (React, Vue, plain HTML). Data in via JS properties; interactions out via non-bubbling CustomEvents.
|
|
32
|
+
|
|
33
|
+
- `<kitn-chat>` — full chat UI (message list + prompt input). The primary starting point.
|
|
34
|
+
- `<kitn-conversation-list>` — sidebar conversation browser with group support.
|
|
35
|
+
- `<kitn-prompt-input>` — standalone composer with send button.
|
|
36
|
+
|
|
37
|
+
**Layer 2 — composable primitives** (`import { … } from '@kitnai/chat'`):
|
|
38
|
+
All 28 elements are also exported individually. Use them for custom layouts or features `<kitn-chat>` does not expose (ChainOfThought, FeedbackBar, ThinkingBar, VoiceInput, …). Your bundler tree-shakes the rest.
|
|
39
|
+
|
|
40
|
+
## Key rules for the web components
|
|
41
|
+
|
|
42
|
+
1. **Array/object data = JS properties** (see above). Scalars may be attributes.
|
|
43
|
+
2. **Events are non-bubbling `CustomEvent`s** — listen directly on the element:
|
|
44
|
+
`chat.addEventListener('submit', (e) => console.log(e.detail.value))`
|
|
45
|
+
3. **`theme` attribute** (`'light' | 'dark' | 'auto'`) works on every element. Default `auto` follows `prefers-color-scheme`.
|
|
46
|
+
4. **Theming via CSS custom properties** — override `--kitn-color-*` tokens on `:root`; they pierce Shadow DOM.
|
|
47
|
+
|
|
48
|
+
## ChatMessage schema (required for `<kitn-chat>`)
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
interface ChatMessage {
|
|
52
|
+
id: string;
|
|
53
|
+
role: 'user' | 'assistant';
|
|
54
|
+
content: string;
|
|
55
|
+
reasoning?: { text: string; label?: string };
|
|
56
|
+
tools?: ToolPart[];
|
|
57
|
+
attachments?: AttachmentData[];
|
|
58
|
+
actions?: ('copy' | 'like' | 'dislike' | 'regenerate' | 'edit')[];
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Framework wiring
|
|
63
|
+
|
|
64
|
+
**Plain HTML / CDN**
|
|
65
|
+
```html
|
|
66
|
+
<script type="module" src="https://unpkg.com/@kitnai/chat/elements"></script>
|
|
67
|
+
<kitn-chat style="display:block;height:100vh"></kitn-chat>
|
|
68
|
+
<script type="module">
|
|
69
|
+
const chat = document.querySelector('kitn-chat');
|
|
70
|
+
chat.messages = [];
|
|
71
|
+
</script>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**React** — typed wrappers auto-set properties and expose `on<Event>` props:
|
|
75
|
+
```tsx
|
|
76
|
+
import { KitnChat } from '@kitnai/chat/react';
|
|
77
|
+
<KitnChat messages={messages} onSubmit={(e) => send(e.detail.value)} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Vue** — use the element directly; pass arrays via `.prop`:
|
|
81
|
+
```vue
|
|
82
|
+
<kitn-chat :messages.prop="messages" @submit="send" />
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Theming
|
|
86
|
+
|
|
87
|
+
```css
|
|
88
|
+
:root {
|
|
89
|
+
--kitn-color-background: #0f0f0f;
|
|
90
|
+
--kitn-color-primary: #7c3aed;
|
|
91
|
+
--kitn-color-muted: #1e1e1e;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
For plain HTML/CDN: `<link rel="stylesheet" href="…/@kitnai/chat/theme.tokens.css">`.
|
|
96
|
+
For Tailwind builds: `@import "@kitnai/chat/theme.css"` in your CSS.
|
|
97
|
+
|
|
98
|
+
## Docs
|
|
99
|
+
|
|
100
|
+
- Full element reference (all 28 elements, every prop/event): ./llms-full.txt — https://kitn.dev/llms-full.txt
|
|
101
|
+
- Machine-readable Custom Elements Manifest: https://unpkg.com/@kitnai/chat/dist/custom-elements.json
|
|
102
|
+
- Working examples: https://github.com/kitn-ai/chat/tree/main/examples
|
|
103
|
+
- Storybook: https://storybook.kitn.dev
|
|
104
|
+
- Repository: https://github.com/kitn-ai/chat
|
package/package.json
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitnai/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"web-components",
|
|
8
|
+
"custom-elements",
|
|
9
|
+
"shadow-dom",
|
|
10
|
+
"ai",
|
|
11
|
+
"chat",
|
|
12
|
+
"chatbot",
|
|
13
|
+
"llm",
|
|
14
|
+
"chat-ui",
|
|
15
|
+
"streaming",
|
|
16
|
+
"markdown",
|
|
17
|
+
"framework-agnostic",
|
|
18
|
+
"react",
|
|
19
|
+
"vue",
|
|
20
|
+
"angular",
|
|
21
|
+
"svelte",
|
|
22
|
+
"solid-js"
|
|
23
|
+
],
|
|
6
24
|
"license": "MIT",
|
|
7
25
|
"homepage": "https://github.com/kitn-ai/chat#readme",
|
|
8
26
|
"repository": {
|
|
@@ -16,22 +34,37 @@
|
|
|
16
34
|
"types": "src/index.ts",
|
|
17
35
|
"unpkg": "./dist/kitn-chat.es.js",
|
|
18
36
|
"jsdelivr": "./dist/kitn-chat.es.js",
|
|
37
|
+
"customElements": "dist/custom-elements.json",
|
|
19
38
|
"exports": {
|
|
20
39
|
".": "./src/index.ts",
|
|
21
|
-
"./elements":
|
|
22
|
-
|
|
40
|
+
"./elements": {
|
|
41
|
+
"types": "./src/elements/element-types.d.ts",
|
|
42
|
+
"default": "./dist/kitn-chat.es.js"
|
|
43
|
+
},
|
|
44
|
+
"./theme.css": "./theme.css",
|
|
45
|
+
"./theme.tokens.css": "./dist/theme.tokens.css",
|
|
46
|
+
"./react": "./frameworks/react/index.tsx"
|
|
23
47
|
},
|
|
24
48
|
"files": [
|
|
25
49
|
"dist",
|
|
26
50
|
"src",
|
|
27
|
-
"
|
|
51
|
+
"frameworks",
|
|
52
|
+
"theme.css",
|
|
53
|
+
"llms.txt",
|
|
54
|
+
"llms-full.txt"
|
|
28
55
|
],
|
|
29
56
|
"scripts": {
|
|
30
57
|
"prepublishOnly": "npm run build",
|
|
31
58
|
"prebuild": "npm run build:css",
|
|
32
59
|
"build": "vite build --config vite.config.ts",
|
|
60
|
+
"postbuild": "npm run build:theme && npm run build:api",
|
|
61
|
+
"build:theme": "node scripts/build-theme-tokens.mjs",
|
|
62
|
+
"build:api": "node scripts/gen-element-api.mjs",
|
|
33
63
|
"test": "vitest run",
|
|
64
|
+
"test:react": "vitest run --config vitest.react.config.ts",
|
|
34
65
|
"test:watch": "vitest",
|
|
66
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.react.json && tsc --noEmit -p tsconfig.react.test.json",
|
|
67
|
+
"examples": "echo 'Serving repo root on http://localhost:8000 — open http://localhost:8000/examples/composable/index.html' && python3 -m http.server 8000",
|
|
35
68
|
"dev": "npm run build:css && storybook dev -p 6006",
|
|
36
69
|
"storybook": "npm run build:css && storybook dev -p 6006",
|
|
37
70
|
"build-storybook": "npm run build:css && storybook build",
|
|
@@ -39,7 +72,7 @@
|
|
|
39
72
|
"build:css:watch": "tailwindcss -i src/elements/styles.css -o src/elements/compiled.css --watch"
|
|
40
73
|
},
|
|
41
74
|
"dependencies": {
|
|
42
|
-
"@
|
|
75
|
+
"@floating-ui/dom": "^1.7.6",
|
|
43
76
|
"@shikijs/langs": "^4.2.0",
|
|
44
77
|
"@shikijs/themes": "^4.2.0",
|
|
45
78
|
"class-variance-authority": "^0.7.0",
|
|
@@ -60,10 +93,18 @@
|
|
|
60
93
|
"@tailwindcss/postcss": "^4.2.2",
|
|
61
94
|
"@tailwindcss/typography": "^0.5.19",
|
|
62
95
|
"@testing-library/jest-dom": "^6.0.0",
|
|
96
|
+
"@testing-library/react": "^16.3.2",
|
|
97
|
+
"@types/react": "^19.2.17",
|
|
98
|
+
"@types/react-dom": "^19.2.3",
|
|
99
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
63
100
|
"@vitest/browser-playwright": "4.1.2",
|
|
64
101
|
"@vitest/coverage-v8": "4.1.2",
|
|
102
|
+
"axe-core": "^4.12.1",
|
|
65
103
|
"jsdom": "^24.0.0",
|
|
66
104
|
"playwright": "^1.59.1",
|
|
105
|
+
"react": "^19.2.7",
|
|
106
|
+
"react-dom": "^19.2.7",
|
|
107
|
+
"remark-gfm": "^4.0.1",
|
|
67
108
|
"storybook": "^10.3.5",
|
|
68
109
|
"storybook-dark-mode": "^5.0.0",
|
|
69
110
|
"storybook-solidjs-vite": "^10.0.12",
|
|
@@ -75,6 +116,12 @@
|
|
|
75
116
|
"vitest": "^4.1.0"
|
|
76
117
|
},
|
|
77
118
|
"peerDependencies": {
|
|
119
|
+
"react": ">=18",
|
|
78
120
|
"solid-js": "^1.9.0"
|
|
121
|
+
},
|
|
122
|
+
"peerDependenciesMeta": {
|
|
123
|
+
"react": {
|
|
124
|
+
"optional": true
|
|
125
|
+
}
|
|
79
126
|
}
|
|
80
127
|
}
|
|
@@ -255,8 +255,10 @@ function AttachmentInfo(props: AttachmentInfoProps) {
|
|
|
255
255
|
<Show when={ctx.variant !== 'grid'}>
|
|
256
256
|
<div class={cn('min-w-0 flex-1', local.class)} {...rest}>
|
|
257
257
|
<span class="block truncate">{label()}</span>
|
|
258
|
-
|
|
259
|
-
|
|
258
|
+
{/* The media-type subtitle is a two-line affordance — only the `list`
|
|
259
|
+
variant has room for it; `inline` chips are a fixed single-line height. */}
|
|
260
|
+
<Show when={local.showMediaType && ctx.variant === 'list' && ctx.data.mediaType}>
|
|
261
|
+
<span class="text-muted-foreground text-caption block truncate">
|
|
260
262
|
{ctx.data.mediaType}
|
|
261
263
|
</span>
|
|
262
264
|
</Show>
|
|
@@ -33,7 +33,7 @@ function ChainOfThoughtTrigger(props: ChainOfThoughtTriggerProps) {
|
|
|
33
33
|
return (
|
|
34
34
|
<CollapsibleTrigger
|
|
35
35
|
class={cn(
|
|
36
|
-
'group text-muted-foreground hover:text-foreground flex cursor-pointer items-center justify-start gap-1 text-left text-
|
|
36
|
+
'group text-muted-foreground hover:text-foreground flex cursor-pointer items-center justify-start gap-1 text-left text-meta transition-colors',
|
|
37
37
|
props.class
|
|
38
38
|
)}
|
|
39
39
|
>
|
|
@@ -26,7 +26,7 @@ export function ChatScopePicker(props: ChatScopePickerProps) {
|
|
|
26
26
|
<DropdownContent class="min-w-[180px]">
|
|
27
27
|
<DropdownItem onSelect={() => local.onScopeChange(undefined)}>All Content</DropdownItem>
|
|
28
28
|
<Show when={local.availableAuthors?.length}>
|
|
29
|
-
<div class="px-2 py-1 text-[10px] uppercase tracking-wider text-muted-foreground
|
|
29
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wider text-muted-foreground">Authors</div>
|
|
30
30
|
<For each={local.availableAuthors}>
|
|
31
31
|
{(author) => (
|
|
32
32
|
<DropdownItem onSelect={() => local.onScopeChange({ authors: [author] })}>{author}</DropdownItem>
|
|
@@ -34,7 +34,7 @@ export function ChatScopePicker(props: ChatScopePickerProps) {
|
|
|
34
34
|
</For>
|
|
35
35
|
</Show>
|
|
36
36
|
<Show when={local.availableTags?.length}>
|
|
37
|
-
<div class="px-2 py-1 text-[10px] uppercase tracking-wider text-muted-foreground
|
|
37
|
+
<div class="px-2 py-1 text-[10px] uppercase tracking-wider text-muted-foreground">Tags</div>
|
|
38
38
|
<For each={local.availableTags}>
|
|
39
39
|
{(tag) => (
|
|
40
40
|
<DropdownItem onSelect={() => local.onScopeChange({ tags: [tag] })}>{tag}</DropdownItem>
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createSignal, For, Show } from 'solid-js';
|
|
2
|
+
import { ChatConfig, useChatConfig } from '../primitives/chat-config';
|
|
3
|
+
import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from './chat-container';
|
|
4
|
+
import { Message, MessageContent, MessageActions } from './message';
|
|
5
|
+
import { Reasoning, ReasoningTrigger, ReasoningContent } from './reasoning';
|
|
6
|
+
import { Tool } from './tool';
|
|
7
|
+
import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, type AttachmentData } from './attachments';
|
|
8
|
+
import { ModelSwitcher } from './model-switcher';
|
|
9
|
+
import { ScrollButton } from './scroll-button';
|
|
10
|
+
import {
|
|
11
|
+
Context, ContextTrigger, ContextContent, ContextContentHeader,
|
|
12
|
+
ContextContentBody, ContextContentFooter, ContextInputUsage, ContextOutputUsage,
|
|
13
|
+
} from './context';
|
|
14
|
+
import { Button } from '../ui/button';
|
|
15
|
+
import { Copy, ThumbsUp, ThumbsDown, RefreshCw, Pencil } from 'lucide-solid';
|
|
16
|
+
import type { Component } from 'solid-js';
|
|
17
|
+
import { DefaultPromptInput } from '../elements/default-input';
|
|
18
|
+
import type { SlashCommandItem } from './slash-command';
|
|
19
|
+
import type { ChatMessage, ChatMessageAction } from '../elements/chat-types';
|
|
20
|
+
import type { ProseSize } from '../primitives/chat-config';
|
|
21
|
+
import type { ModelOption } from '../types';
|
|
22
|
+
|
|
23
|
+
export interface ChatThreadContextUsage {
|
|
24
|
+
usedTokens: number;
|
|
25
|
+
maxTokens: number;
|
|
26
|
+
inputTokens?: number;
|
|
27
|
+
outputTokens?: number;
|
|
28
|
+
estimatedCost?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChatThreadProps {
|
|
32
|
+
/** Extra classes for the thread root (e.g. `h-full`). */
|
|
33
|
+
class?: string;
|
|
34
|
+
/** The full message thread to render, newest last. Each entry carries its role,
|
|
35
|
+
* content, and optional reasoning/tools/attachments/actions. Set as a JS
|
|
36
|
+
* property (`el.messages = [...]`). */
|
|
37
|
+
messages: ChatMessage[];
|
|
38
|
+
/** Controlled value of the input. When set, the host owns the input text and
|
|
39
|
+
* must update it on `valuechange`; leave unset for uncontrolled behavior. */
|
|
40
|
+
value?: string;
|
|
41
|
+
/** Placeholder text shown in the empty input. */
|
|
42
|
+
placeholder?: string;
|
|
43
|
+
/** When true, shows the loading/streaming state and disables submit (use while
|
|
44
|
+
* awaiting the assistant's reply). */
|
|
45
|
+
loading?: boolean;
|
|
46
|
+
/** Starter prompts shown above the input when the thread is empty. Clicking one
|
|
47
|
+
* follows `suggestionMode`. Set as a JS property. */
|
|
48
|
+
suggestions?: string[];
|
|
49
|
+
/** What clicking a suggestion does: `'submit'` (default) sends it immediately
|
|
50
|
+
* as if typed and submitted; `'fill'` just places it in the input. */
|
|
51
|
+
suggestionMode?: 'submit' | 'fill';
|
|
52
|
+
/** Body/prose font scale for rendered markdown (`'xs' | 'sm' | 'base' | 'lg'`).
|
|
53
|
+
* Defaults to `'sm'`. */
|
|
54
|
+
proseSize?: ProseSize;
|
|
55
|
+
/** Shiki theme name for syntax-highlighted code blocks (e.g.
|
|
56
|
+
* `'github-dark-dimmed'`). */
|
|
57
|
+
codeTheme?: string;
|
|
58
|
+
/** Enable Shiki syntax highlighting in code blocks. Turn off to render plain
|
|
59
|
+
* `<pre>` blocks (lighter, no highlighter load). Default true. */
|
|
60
|
+
codeHighlight?: boolean;
|
|
61
|
+
/** Optional header title shown on the left of the header. */
|
|
62
|
+
chatTitle?: string;
|
|
63
|
+
/** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
|
|
64
|
+
* header and a `modelchange` event fires on selection. */
|
|
65
|
+
models?: ModelOption[];
|
|
66
|
+
/** The currently selected model id (pairs with `models`). */
|
|
67
|
+
currentModel?: string;
|
|
68
|
+
/** Optional context-window token usage. When set, a Context token meter is
|
|
69
|
+
* shown in the header. */
|
|
70
|
+
context?: ChatThreadContextUsage;
|
|
71
|
+
/** Show the scroll-to-bottom button inside the scroll area. Default true. */
|
|
72
|
+
scrollButton?: boolean;
|
|
73
|
+
/** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
|
|
74
|
+
search?: boolean;
|
|
75
|
+
/** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
|
|
76
|
+
voice?: boolean;
|
|
77
|
+
/** Slash commands — when set, typing `/` in the input opens the command
|
|
78
|
+
* palette and fires `slashselect`. Set as a JS property. */
|
|
79
|
+
slashCommands?: SlashCommandItem[];
|
|
80
|
+
/** Command ids to highlight as active in the palette. */
|
|
81
|
+
slashActiveIds?: string[];
|
|
82
|
+
/** Single-line palette rows. */
|
|
83
|
+
slashCompact?: boolean;
|
|
84
|
+
// callbacks (the facade maps these to dispatch())
|
|
85
|
+
onValueChange?: (value: string) => void;
|
|
86
|
+
onSubmit?: (detail: { value: string; attachments: AttachmentData[] }) => void;
|
|
87
|
+
onSuggestionClick?: (value: string) => void;
|
|
88
|
+
onModelChange?: (modelId: string) => void;
|
|
89
|
+
onMessageAction?: (detail: { messageId: string; action: ChatMessageAction }) => void;
|
|
90
|
+
onSearch?: () => void;
|
|
91
|
+
onVoice?: () => void;
|
|
92
|
+
onSlashSelect?: (command: SlashCommandItem) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const ACTION_LABEL: Record<ChatMessageAction, string> = {
|
|
96
|
+
copy: 'Copy', like: 'Like', dislike: 'Dislike', regenerate: 'Regenerate', edit: 'Edit',
|
|
97
|
+
};
|
|
98
|
+
const ACTION_ICON: Record<ChatMessageAction, Component<{ class?: string }>> = {
|
|
99
|
+
copy: Copy, like: ThumbsUp, dislike: ThumbsDown, regenerate: RefreshCw, edit: Pencil,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export function ChatThread(props: ChatThreadProps) {
|
|
103
|
+
const outer = useChatConfig();
|
|
104
|
+
const [internal, setInternal] = createSignal(props.value ?? '');
|
|
105
|
+
const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
|
|
106
|
+
const current = () => props.value ?? internal();
|
|
107
|
+
const handleChange = (v: string) => { setInternal(v); props.onValueChange?.(v); };
|
|
108
|
+
const handleSubmit = () => { props.onSubmit?.({ value: current(), attachments: attachments() }); setAttachments([]); };
|
|
109
|
+
const handleSuggestionClick = (v: string) => {
|
|
110
|
+
if ((props.suggestionMode ?? 'submit') === 'fill') { handleChange(v); props.onSuggestionClick?.(v); }
|
|
111
|
+
else { props.onSubmit?.({ value: v, attachments: attachments() }); setAttachments([]); }
|
|
112
|
+
};
|
|
113
|
+
const showHeader = () => !!(props.chatTitle || props.models || props.context);
|
|
114
|
+
const showScrollButton = () => props.scrollButton !== false;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<ChatConfig proseSize={props.proseSize} codeTheme={props.codeTheme} codeHighlight={props.codeHighlight !== false} portalMount={outer.portalMount()}>
|
|
118
|
+
<div class={`flex h-full flex-col bg-background ${props.class ?? ''}`}>
|
|
119
|
+
<Show when={showHeader()}>
|
|
120
|
+
<header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
|
|
121
|
+
<div class="text-sm font-semibold text-foreground">{props.chatTitle}</div>
|
|
122
|
+
<div class="flex items-center gap-2">
|
|
123
|
+
<Show when={props.models}>
|
|
124
|
+
<ModelSwitcher
|
|
125
|
+
models={props.models!}
|
|
126
|
+
currentModelId={props.currentModel ?? props.models![0]?.id ?? ''}
|
|
127
|
+
onModelChange={(modelId) => props.onModelChange?.(modelId)}
|
|
128
|
+
/>
|
|
129
|
+
</Show>
|
|
130
|
+
<Show when={props.context}>
|
|
131
|
+
<Context
|
|
132
|
+
usedTokens={props.context!.usedTokens} maxTokens={props.context!.maxTokens}
|
|
133
|
+
inputTokens={props.context!.inputTokens} outputTokens={props.context!.outputTokens}
|
|
134
|
+
estimatedCost={props.context!.estimatedCost}
|
|
135
|
+
>
|
|
136
|
+
<ContextTrigger />
|
|
137
|
+
<ContextContent>
|
|
138
|
+
<ContextContentHeader />
|
|
139
|
+
<ContextContentBody><div class="space-y-1.5"><ContextInputUsage /><ContextOutputUsage /></div></ContextContentBody>
|
|
140
|
+
<ContextContentFooter />
|
|
141
|
+
</ContextContent>
|
|
142
|
+
</Context>
|
|
143
|
+
</Show>
|
|
144
|
+
</div>
|
|
145
|
+
</header>
|
|
146
|
+
</Show>
|
|
147
|
+
<div class="relative flex-1 overflow-hidden">
|
|
148
|
+
<ChatContainer class="h-full px-4 py-3">
|
|
149
|
+
<ChatContainerContent class="mx-auto w-full max-w-3xl space-y-4">
|
|
150
|
+
<For each={props.messages}>
|
|
151
|
+
{(m) => (
|
|
152
|
+
<Message class={m.role === 'user' ? 'flex-col items-end' : 'flex-col items-start'}>
|
|
153
|
+
<Show when={m.reasoning}>
|
|
154
|
+
<Reasoning class="mb-2 w-full">
|
|
155
|
+
<ReasoningTrigger>{m.reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
|
|
156
|
+
<ReasoningContent markdown>{m.reasoning!.text}</ReasoningContent>
|
|
157
|
+
</Reasoning>
|
|
158
|
+
</Show>
|
|
159
|
+
<For each={m.tools ?? []}>{(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}</For>
|
|
160
|
+
<Show when={m.attachments?.length}>
|
|
161
|
+
<Attachments variant="inline" class={m.role === 'user' ? 'mb-2 justify-end' : 'mb-2'}>
|
|
162
|
+
<For each={m.attachments!}>
|
|
163
|
+
{(att) => (<Attachment data={att}><AttachmentPreview /><AttachmentInfo /></Attachment>)}
|
|
164
|
+
</For>
|
|
165
|
+
</Attachments>
|
|
166
|
+
</Show>
|
|
167
|
+
<MessageContent
|
|
168
|
+
markdown={m.role === 'assistant'}
|
|
169
|
+
class={m.role === 'user' ? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2' : 'bg-transparent p-0'}
|
|
170
|
+
>
|
|
171
|
+
{m.content}
|
|
172
|
+
</MessageContent>
|
|
173
|
+
<Show when={m.actions?.length}>
|
|
174
|
+
<MessageActions class="mt-1 flex gap-0">
|
|
175
|
+
<For each={m.actions!}>
|
|
176
|
+
{(a) => (
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost" size="icon-sm" class="rounded-full"
|
|
179
|
+
data-action={a} aria-label={ACTION_LABEL[a]}
|
|
180
|
+
onClick={() => props.onMessageAction?.({ messageId: m.id, action: a })}
|
|
181
|
+
>
|
|
182
|
+
{(() => { const Icon = ACTION_ICON[a]; return <Icon class="size-3.5" />; })()}
|
|
183
|
+
</Button>
|
|
184
|
+
)}
|
|
185
|
+
</For>
|
|
186
|
+
</MessageActions>
|
|
187
|
+
</Show>
|
|
188
|
+
</Message>
|
|
189
|
+
)}
|
|
190
|
+
</For>
|
|
191
|
+
<ChatContainerScrollAnchor />
|
|
192
|
+
</ChatContainerContent>
|
|
193
|
+
<Show when={showScrollButton()}>
|
|
194
|
+
<div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
|
|
195
|
+
<ScrollButton class="shadow-sm" />
|
|
196
|
+
</div>
|
|
197
|
+
</Show>
|
|
198
|
+
</ChatContainer>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="shrink-0 px-4 pb-4">
|
|
201
|
+
<div class="mx-auto max-w-3xl">
|
|
202
|
+
<DefaultPromptInput
|
|
203
|
+
value={current()} placeholder={props.placeholder} loading={props.loading === true}
|
|
204
|
+
suggestions={props.suggestions} attachments={attachments()}
|
|
205
|
+
search={props.search === true} voice={props.voice === true}
|
|
206
|
+
slashCommands={props.slashCommands} slashActiveIds={props.slashActiveIds} slashCompact={props.slashCompact === true}
|
|
207
|
+
onValueChange={handleChange} onSubmit={handleSubmit} onSuggestionClick={handleSuggestionClick}
|
|
208
|
+
onAttachmentsChange={setAttachments}
|
|
209
|
+
onSearch={() => props.onSearch?.()} onVoice={() => props.onVoice?.()}
|
|
210
|
+
onSlashSelect={(command) => props.onSlashSelect?.(command)}
|
|
211
|
+
/>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</ChatConfig>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
@@ -61,7 +61,11 @@ export function CheckpointTrigger(props: CheckpointTriggerProps) {
|
|
|
61
61
|
const variant = () => props.variant ?? 'ghost';
|
|
62
62
|
const size = () => props.size ?? 'sm';
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
// A factory (not a single shared node): the Show fallback and the Tooltip
|
|
65
|
+
// child each need their OWN element instance, otherwise Solid reuses one DOM
|
|
66
|
+
// node across both branches and throws HierarchyRequestError when the tooltip
|
|
67
|
+
// branch mounts it.
|
|
68
|
+
const renderButton = () => (
|
|
65
69
|
<Button
|
|
66
70
|
variant={variant()}
|
|
67
71
|
size={size()}
|
|
@@ -74,8 +78,8 @@ export function CheckpointTrigger(props: CheckpointTriggerProps) {
|
|
|
74
78
|
);
|
|
75
79
|
|
|
76
80
|
return (
|
|
77
|
-
<Show when={props.tooltip} fallback={
|
|
78
|
-
<Tooltip content={props.tooltip!}>{
|
|
81
|
+
<Show when={props.tooltip} fallback={renderButton()}>
|
|
82
|
+
<Tooltip content={props.tooltip!}>{renderButton()}</Tooltip>
|
|
79
83
|
</Show>
|
|
80
84
|
);
|
|
81
85
|
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { type JSX, Show, createContext, createMemo,
|
|
2
|
-
import {
|
|
1
|
+
import { type JSX, Show, createContext, createMemo, useContext } from 'solid-js';
|
|
2
|
+
import { HoverCardRoot, HoverCardTrigger, HoverCardContent } from '../ui/hover-card';
|
|
3
3
|
import { cn } from '../utils/cn';
|
|
4
4
|
import { Button } from '../ui/button';
|
|
5
|
-
import { useChatConfig } from '../primitives/chat-config';
|
|
6
5
|
|
|
7
6
|
const ICON_RADIUS = 10;
|
|
8
7
|
const ICON_VIEWBOX = 24;
|
|
@@ -60,9 +59,9 @@ export function Context(props: ContextProps) {
|
|
|
60
59
|
|
|
61
60
|
return (
|
|
62
61
|
<ContextCtx.Provider value={value()}>
|
|
63
|
-
<
|
|
62
|
+
<HoverCardRoot openDelay={0}>
|
|
64
63
|
{props.children}
|
|
65
|
-
</
|
|
64
|
+
</HoverCardRoot>
|
|
66
65
|
</ContextCtx.Provider>
|
|
67
66
|
);
|
|
68
67
|
}
|
|
@@ -123,7 +122,7 @@ export function ContextTrigger(props: ContextTriggerProps) {
|
|
|
123
122
|
const renderedPercent = createMemo(() => fmtPercent.format(usedPercent()));
|
|
124
123
|
|
|
125
124
|
return (
|
|
126
|
-
<
|
|
125
|
+
<HoverCardTrigger>
|
|
127
126
|
<Show
|
|
128
127
|
when={!props.children}
|
|
129
128
|
fallback={props.children}
|
|
@@ -133,7 +132,7 @@ export function ContextTrigger(props: ContextTriggerProps) {
|
|
|
133
132
|
<ContextIcon />
|
|
134
133
|
</Button>
|
|
135
134
|
</Show>
|
|
136
|
-
</
|
|
135
|
+
</HoverCardTrigger>
|
|
137
136
|
);
|
|
138
137
|
}
|
|
139
138
|
|
|
@@ -145,18 +144,15 @@ export interface ContextContentProps {
|
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
export function ContextContent(props: ContextContentProps) {
|
|
148
|
-
const config = useChatConfig();
|
|
149
147
|
return (
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
</KHoverCard.Content>
|
|
159
|
-
</KHoverCard.Portal>
|
|
148
|
+
<HoverCardContent
|
|
149
|
+
class={cn(
|
|
150
|
+
'min-w-60 divide-y divide-border overflow-hidden',
|
|
151
|
+
props.class
|
|
152
|
+
)}
|
|
153
|
+
>
|
|
154
|
+
{props.children}
|
|
155
|
+
</HoverCardContent>
|
|
160
156
|
);
|
|
161
157
|
}
|
|
162
158
|
|
|
@@ -10,7 +10,7 @@ export function ConversationItem(props: ConversationItemProps) {
|
|
|
10
10
|
<button data-conversation-id={local.conversation.id} onClick={() => local.onSelect(local.conversation.id)}
|
|
11
11
|
class={cn('w-full text-left rounded-lg px-2.5 py-2 transition-colors', local.isActive ? 'bg-muted' : 'hover:bg-muted/50', local.class)}>
|
|
12
12
|
<div class={cn('truncate text-sm', local.isActive ? 'text-foreground font-medium' : 'text-muted-foreground')}>{local.conversation.title}</div>
|
|
13
|
-
<div class="text-muted-foreground
|
|
13
|
+
<div class="text-muted-foreground truncate mt-0.5 text-xs">{local.conversation.messageCount} messages</div>
|
|
14
14
|
</button>
|
|
15
15
|
);
|
|
16
16
|
}
|
|
@@ -43,20 +43,21 @@ export function ConversationList(props: ConversationListProps) {
|
|
|
43
43
|
<div class={cn('flex flex-col h-full bg-sidebar', local.class)}>
|
|
44
44
|
<div class="flex items-center justify-between p-3 pb-2">
|
|
45
45
|
<div class="flex items-center gap-2">
|
|
46
|
-
<Button variant="ghost" size="icon-sm" onClick={local.onToggleSidebar}>
|
|
46
|
+
<Button variant="ghost" size="icon-sm" aria-label="Toggle sidebar" onClick={local.onToggleSidebar}>
|
|
47
47
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
48
48
|
</Button>
|
|
49
49
|
<span class="text-sm font-semibold text-foreground">Chats</span>
|
|
50
50
|
</div>
|
|
51
|
-
<Button variant="ghost" size="icon-sm" onClick={local.onNewChat}>
|
|
51
|
+
<Button variant="ghost" size="icon-sm" aria-label="New chat" onClick={local.onNewChat}>
|
|
52
52
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
53
53
|
</Button>
|
|
54
54
|
</div>
|
|
55
55
|
<div class="px-3 pb-2">
|
|
56
56
|
<div class="flex items-center gap-2 rounded-md bg-muted/40 px-2.5 py-1.5">
|
|
57
|
-
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted-foreground
|
|
57
|
+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-muted-foreground"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|
58
58
|
<input type="text" value={searchQuery()} onInput={(e) => setSearchQuery(e.currentTarget.value)} placeholder="Search chats..."
|
|
59
|
-
|
|
59
|
+
aria-label="Search chats"
|
|
60
|
+
class="bg-transparent text-[13px] text-foreground placeholder:text-muted-foreground rounded-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring w-full" />
|
|
60
61
|
</div>
|
|
61
62
|
</div>
|
|
62
63
|
<ScrollArea class="flex-1 px-2">
|
|
@@ -23,7 +23,7 @@ function MessageSkills(props: MessageSkillsProps) {
|
|
|
23
23
|
<div class={cn("flex items-center gap-1 flex-wrap", props.class)}>
|
|
24
24
|
<For each={props.skills}>
|
|
25
25
|
{(skill) => (
|
|
26
|
-
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-violet-400/10 text-violet-400">
|
|
26
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-violet-400/10 text-violet-600 dark:text-violet-400">
|
|
27
27
|
{skill.name}
|
|
28
28
|
</span>
|
|
29
29
|
)}
|
|
@@ -133,6 +133,7 @@ function MessageCopyButton(props: MessageCopyButtonProps) {
|
|
|
133
133
|
return (
|
|
134
134
|
<button
|
|
135
135
|
class={props.class}
|
|
136
|
+
aria-label={copied() ? 'Copied' : 'Copy message'}
|
|
136
137
|
onClick={() => {
|
|
137
138
|
navigator.clipboard.writeText(props.content);
|
|
138
139
|
setCopied(true);
|
|
@@ -13,7 +13,7 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
|
|
|
13
13
|
<Show when={local.models.length > 1}>
|
|
14
14
|
<Dropdown>
|
|
15
15
|
<DropdownTrigger as={(triggerProps: any) => (
|
|
16
|
-
<Button variant="ghost" size="sm" class={cn('gap-1 text-
|
|
16
|
+
<Button variant="ghost" size="sm" class={cn('gap-1 text-meta text-muted-foreground', local.class)} {...triggerProps}>
|
|
17
17
|
{currentModel()?.name ?? local.currentModelId}
|
|
18
18
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
|
19
19
|
</Button>
|
|
@@ -23,8 +23,8 @@ export function ModelSwitcher(props: ModelSwitcherProps) {
|
|
|
23
23
|
{(model) => (
|
|
24
24
|
<DropdownItem onSelect={() => local.onModelChange(model.id)}>
|
|
25
25
|
<div class="flex flex-col">
|
|
26
|
-
<span class={cn('text-
|
|
27
|
-
<Show when={model.provider}><span class="text-
|
|
26
|
+
<span class={cn('text-body', model.id === local.currentModelId && 'font-medium text-foreground')}>{model.name}</span>
|
|
27
|
+
<Show when={model.provider}><span class="text-caption text-muted-foreground">{model.provider}</span></Show>
|
|
28
28
|
</div>
|
|
29
29
|
</DropdownItem>
|
|
30
30
|
)}
|