@kitnai/chat 0.3.1 → 0.4.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 +11 -0
- package/dist/custom-elements.json +2494 -0
- package/dist/kitn-chat.es.js +52 -39
- package/dist/llms/llms-full.txt +667 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/theme.tokens.css +133 -0
- package/frameworks/react/index.tsx +530 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +667 -0
- package/llms.txt +104 -0
- package/package.json +34 -5
- 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/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 +15 -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.tsx +51 -7
- 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 +97 -11
- package/src/elements/element-types.d.ts +404 -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-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 +31 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/source.tsx +67 -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 +2 -2
- 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 +2 -2
- package/src/stories/docs/Integrations.mdx +415 -15
- package/src/stories/docs/Introduction.mdx +5 -5
- package/src/stories/docs/Theming.mdx +1 -1
- package/src/stories/typography.stories.tsx +78 -0
- package/src/ui/button.tsx +1 -1
- package/src/ui/collapsible.tsx +119 -8
- package/src/ui/dropdown.tsx +177 -12
- package/src/ui/hover-card.tsx +147 -26
- package/src/ui/overlay.tsx +151 -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 +72 -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
|
+
> SolidJS + Shadow-DOM web component kit for building AI chat interfaces. 27 `kitn-*` custom elements: streaming responses, markdown + code rendering, reasoning/tool panels, attachments, conversation sidebar, voice input. Zero framework dependency for consumers; SolidJS is bundled in.
|
|
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 27 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 27 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kitnai/chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SolidJS + Shadow-DOM web component kit for building AI chat interfaces.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,22 +16,37 @@
|
|
|
16
16
|
"types": "src/index.ts",
|
|
17
17
|
"unpkg": "./dist/kitn-chat.es.js",
|
|
18
18
|
"jsdelivr": "./dist/kitn-chat.es.js",
|
|
19
|
+
"customElements": "dist/custom-elements.json",
|
|
19
20
|
"exports": {
|
|
20
21
|
".": "./src/index.ts",
|
|
21
|
-
"./elements":
|
|
22
|
-
|
|
22
|
+
"./elements": {
|
|
23
|
+
"types": "./src/elements/element-types.d.ts",
|
|
24
|
+
"default": "./dist/kitn-chat.es.js"
|
|
25
|
+
},
|
|
26
|
+
"./theme.css": "./theme.css",
|
|
27
|
+
"./theme.tokens.css": "./dist/theme.tokens.css",
|
|
28
|
+
"./react": "./frameworks/react/index.tsx"
|
|
23
29
|
},
|
|
24
30
|
"files": [
|
|
25
31
|
"dist",
|
|
26
32
|
"src",
|
|
27
|
-
"
|
|
33
|
+
"frameworks",
|
|
34
|
+
"theme.css",
|
|
35
|
+
"llms.txt",
|
|
36
|
+
"llms-full.txt"
|
|
28
37
|
],
|
|
29
38
|
"scripts": {
|
|
30
39
|
"prepublishOnly": "npm run build",
|
|
31
40
|
"prebuild": "npm run build:css",
|
|
32
41
|
"build": "vite build --config vite.config.ts",
|
|
42
|
+
"postbuild": "npm run build:theme && npm run build:api",
|
|
43
|
+
"build:theme": "node scripts/build-theme-tokens.mjs",
|
|
44
|
+
"build:api": "node scripts/gen-element-api.mjs",
|
|
33
45
|
"test": "vitest run",
|
|
46
|
+
"test:react": "vitest run --config vitest.react.config.ts",
|
|
34
47
|
"test:watch": "vitest",
|
|
48
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.react.json && tsc --noEmit -p tsconfig.react.test.json",
|
|
49
|
+
"examples": "echo 'Serving repo root on http://localhost:8000 — open http://localhost:8000/examples/composable/index.html' && python3 -m http.server 8000",
|
|
35
50
|
"dev": "npm run build:css && storybook dev -p 6006",
|
|
36
51
|
"storybook": "npm run build:css && storybook dev -p 6006",
|
|
37
52
|
"build-storybook": "npm run build:css && storybook build",
|
|
@@ -39,7 +54,7 @@
|
|
|
39
54
|
"build:css:watch": "tailwindcss -i src/elements/styles.css -o src/elements/compiled.css --watch"
|
|
40
55
|
},
|
|
41
56
|
"dependencies": {
|
|
42
|
-
"@
|
|
57
|
+
"@floating-ui/dom": "^1.7.6",
|
|
43
58
|
"@shikijs/langs": "^4.2.0",
|
|
44
59
|
"@shikijs/themes": "^4.2.0",
|
|
45
60
|
"class-variance-authority": "^0.7.0",
|
|
@@ -60,10 +75,18 @@
|
|
|
60
75
|
"@tailwindcss/postcss": "^4.2.2",
|
|
61
76
|
"@tailwindcss/typography": "^0.5.19",
|
|
62
77
|
"@testing-library/jest-dom": "^6.0.0",
|
|
78
|
+
"@testing-library/react": "^16.3.2",
|
|
79
|
+
"@types/react": "^19.2.17",
|
|
80
|
+
"@types/react-dom": "^19.2.3",
|
|
81
|
+
"@vitejs/plugin-react": "^4.7.0",
|
|
63
82
|
"@vitest/browser-playwright": "4.1.2",
|
|
64
83
|
"@vitest/coverage-v8": "4.1.2",
|
|
84
|
+
"axe-core": "^4.12.1",
|
|
65
85
|
"jsdom": "^24.0.0",
|
|
66
86
|
"playwright": "^1.59.1",
|
|
87
|
+
"react": "^19.2.7",
|
|
88
|
+
"react-dom": "^19.2.7",
|
|
89
|
+
"remark-gfm": "^4.0.1",
|
|
67
90
|
"storybook": "^10.3.5",
|
|
68
91
|
"storybook-dark-mode": "^5.0.0",
|
|
69
92
|
"storybook-solidjs-vite": "^10.0.12",
|
|
@@ -75,6 +98,12 @@
|
|
|
75
98
|
"vitest": "^4.1.0"
|
|
76
99
|
},
|
|
77
100
|
"peerDependencies": {
|
|
101
|
+
"react": ">=18",
|
|
78
102
|
"solid-js": "^1.9.0"
|
|
103
|
+
},
|
|
104
|
+
"peerDependenciesMeta": {
|
|
105
|
+
"react": {
|
|
106
|
+
"optional": true
|
|
107
|
+
}
|
|
79
108
|
}
|
|
80
109
|
}
|
|
@@ -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>
|
|
@@ -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
|
)}
|
|
@@ -121,8 +121,21 @@ function PromptInputTextarea(props: PromptInputTextareaProps) {
|
|
|
121
121
|
));
|
|
122
122
|
|
|
123
123
|
function handleInput(e: InputEvent & { currentTarget: HTMLTextAreaElement }) {
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const el = e.currentTarget;
|
|
125
|
+
let value = el.value;
|
|
126
|
+
// Disallow leading whitespace — a prompt can't start with a space or blank
|
|
127
|
+
// line. Strip it (covers typing a space at the start AND pasting) and keep
|
|
128
|
+
// the caret in the right place.
|
|
129
|
+
if (/^\s/.test(value)) {
|
|
130
|
+
const stripped = value.replace(/^\s+/, '');
|
|
131
|
+
const removed = value.length - stripped.length;
|
|
132
|
+
const caret = Math.max(0, (el.selectionStart ?? 0) - removed);
|
|
133
|
+
el.value = stripped;
|
|
134
|
+
el.setSelectionRange(caret, caret);
|
|
135
|
+
value = stripped;
|
|
136
|
+
}
|
|
137
|
+
adjustHeight(el);
|
|
138
|
+
ctx.setValue(value);
|
|
126
139
|
}
|
|
127
140
|
|
|
128
141
|
function handleKeyDown(e: KeyboardEvent & { currentTarget: HTMLTextAreaElement }) {
|
|
@@ -74,7 +74,7 @@ function ReasoningTrigger(props: ReasoningTriggerProps) {
|
|
|
74
74
|
|
|
75
75
|
return (
|
|
76
76
|
<button
|
|
77
|
-
class={cn('flex cursor-pointer items-center gap-2', local.class)}
|
|
77
|
+
class={cn('flex cursor-pointer items-center gap-2 text-meta', local.class)}
|
|
78
78
|
onClick={() => onOpenChange(!isOpen())}
|
|
79
79
|
{...rest}
|
|
80
80
|
>
|
|
@@ -142,7 +142,7 @@ function ReasoningContent(props: ReasoningContentProps) {
|
|
|
142
142
|
// Markdown content is styled by the token-based `.chat-markdown` (see
|
|
143
143
|
// Markdown component), which themes via design tokens — so no Tailwind
|
|
144
144
|
// `prose`/`dark:prose-invert` is needed (those wouldn't follow a scoped theme).
|
|
145
|
-
'text-muted-foreground',
|
|
145
|
+
'text-muted-foreground text-body',
|
|
146
146
|
local.contentClass
|
|
147
147
|
)}
|
|
148
148
|
>
|
|
@@ -99,11 +99,20 @@ function SlashCommand(props: SlashCommandProps) {
|
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
function selectItem(item: SlashCommandItem) {
|
|
102
|
-
|
|
102
|
+
// Insert the chosen command into the prompt (e.g. "/summarize ") so it
|
|
103
|
+
// appears in the input ready to send or edit. The trailing space ends the
|
|
104
|
+
// slash token, which closes the palette. Still fire onSelect so consumers
|
|
105
|
+
// can react to the selection.
|
|
106
|
+
ctx.setValue(item.label + " ");
|
|
103
107
|
setOpen(false);
|
|
104
108
|
props.onSelect(item);
|
|
105
|
-
// Refocus textarea
|
|
106
|
-
setTimeout(() =>
|
|
109
|
+
// Refocus the textarea and place the caret at the end.
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
const ta = ctx.textareaRef;
|
|
112
|
+
if (!ta) return;
|
|
113
|
+
ta.focus();
|
|
114
|
+
ta.setSelectionRange(ta.value.length, ta.value.length);
|
|
115
|
+
}, 0);
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
function handleKeyDown(e: KeyboardEvent) {
|
|
@@ -163,7 +172,7 @@ function SlashCommand(props: SlashCommandProps) {
|
|
|
163
172
|
{([category, items]) => (
|
|
164
173
|
<>
|
|
165
174
|
<Show when={category}>
|
|
166
|
-
<div class="px-3 pt-2 pb-1 text-[10px] font-semibold text-muted-foreground
|
|
175
|
+
<div class="px-3 pt-2 pb-1 text-[10px] font-semibold text-muted-foreground uppercase tracking-wide">
|
|
167
176
|
{category}
|
|
168
177
|
</div>
|
|
169
178
|
</Show>
|
|
@@ -189,11 +198,11 @@ function SlashCommand(props: SlashCommandProps) {
|
|
|
189
198
|
<div class="text-xs flex items-center gap-1.5">
|
|
190
199
|
{item.label}
|
|
191
200
|
<Show when={isActive()}>
|
|
192
|
-
<span class="text-[10px] text-violet-400">active</span>
|
|
201
|
+
<span class="text-[10px] text-violet-600 dark:text-violet-400">active</span>
|
|
193
202
|
</Show>
|
|
194
203
|
</div>
|
|
195
204
|
<Show when={item.description}>
|
|
196
|
-
<div class="text-xs text-muted-foreground
|
|
205
|
+
<div class="text-xs text-muted-foreground truncate">
|
|
197
206
|
{item.description}
|
|
198
207
|
</div>
|
|
199
208
|
</Show>
|
|
@@ -202,9 +211,9 @@ function SlashCommand(props: SlashCommandProps) {
|
|
|
202
211
|
<Show when={isActive()}>
|
|
203
212
|
<span class="w-1 h-1 rounded-full bg-violet-400 flex-shrink-0" />
|
|
204
213
|
</Show>
|
|
205
|
-
<span class={cn("text-xs flex-shrink-0", isActive() && "text-violet-400")}>{item.label}</span>
|
|
214
|
+
<span class={cn("text-xs flex-shrink-0", isActive() && "text-violet-600 dark:text-violet-400")}>{item.label}</span>
|
|
206
215
|
<Show when={item.description}>
|
|
207
|
-
<span class="text-xs text-muted-foreground
|
|
216
|
+
<span class="text-xs text-muted-foreground truncate">{item.description}</span>
|
|
208
217
|
</Show>
|
|
209
218
|
</Show>
|
|
210
219
|
</button>
|
|
@@ -35,7 +35,7 @@ function Source(props: SourceProps) {
|
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<SourceContext.Provider value={{ get href() { return props.href; }, get domain() { return domain(); } }}>
|
|
38
|
-
<HoverCardRoot openDelay={150}
|
|
38
|
+
<HoverCardRoot openDelay={150}>
|
|
39
39
|
{props.children}
|
|
40
40
|
</HoverCardRoot>
|
|
41
41
|
</SourceContext.Provider>
|
|
@@ -61,7 +61,7 @@ function SourceTrigger(props: SourceTriggerProps) {
|
|
|
61
61
|
target="_blank"
|
|
62
62
|
rel="noopener noreferrer"
|
|
63
63
|
class={cn(
|
|
64
|
-
'bg-muted text-muted-foreground hover:bg-muted-foreground/30 hover:text-primary inline-flex h-5 max-w-32 items-center gap-1 overflow-hidden rounded-full py-0 text-xs no-underline transition-colors duration-150',
|
|
64
|
+
'bg-muted text-muted-foreground hover:bg-muted-foreground/30 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background inline-flex h-5 max-w-32 items-center gap-1 overflow-hidden rounded-full py-0 text-xs no-underline transition-colors duration-150',
|
|
65
65
|
props.showFavicon ? 'pr-2 pl-1' : 'px-2',
|
|
66
66
|
props.class
|
|
67
67
|
)}
|
|
@@ -22,7 +22,7 @@ function ThinkingBar(props: ThinkingBarProps) {
|
|
|
22
22
|
<Show
|
|
23
23
|
when={local.onClick}
|
|
24
24
|
fallback={
|
|
25
|
-
<TextShimmer class="cursor-default font-medium">{text()}</TextShimmer>
|
|
25
|
+
<TextShimmer class="cursor-default text-sm font-medium">{text()}</TextShimmer>
|
|
26
26
|
}
|
|
27
27
|
>
|
|
28
28
|
<button
|
|
@@ -30,7 +30,7 @@ function ThinkingBar(props: ThinkingBarProps) {
|
|
|
30
30
|
onClick={local.onClick}
|
|
31
31
|
class="flex items-center gap-1 text-sm transition-opacity hover:opacity-80"
|
|
32
32
|
>
|
|
33
|
-
<TextShimmer class="font-medium">{text()}</TextShimmer>
|
|
33
|
+
<TextShimmer class="text-sm font-medium">{text()}</TextShimmer>
|
|
34
34
|
<ChevronRight class="text-muted-foreground size-4" />
|
|
35
35
|
</button>
|
|
36
36
|
</Show>
|
package/src/components/tool.tsx
CHANGED
|
@@ -46,10 +46,19 @@ function ToolStateIcon(props: { state: ToolPart['state'] }) {
|
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Status chips: a
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
49
|
+
// Status chips: a hue as text over a 15% translucent fill of the same hue
|
|
50
|
+
// (mirroring the inline-code chip). The TEXT color comes from a theme token
|
|
51
|
+
// (--color-tool-*) whose light value is darkened so it reaches WCAG AA (4.5:1)
|
|
52
|
+
// on the faint fill, while dark mode keeps a brighter hue for AA on the dark
|
|
53
|
+
// surface — both modes resolve via the token's `.dark` override. The FILL keeps
|
|
54
|
+
// a fixed bright hue so the chip's colored tint looks the same in both modes.
|
|
55
|
+
const STATE_TOKEN: Record<ToolPart['state'], string> = {
|
|
56
|
+
'input-streaming': 'var(--color-tool-blue)',
|
|
57
|
+
'input-available': 'var(--color-tool-amber)',
|
|
58
|
+
'output-available': 'var(--color-tool-green)',
|
|
59
|
+
'output-error': 'var(--color-tool-red)',
|
|
60
|
+
};
|
|
61
|
+
const STATE_FILL: Record<ToolPart['state'], string> = {
|
|
53
62
|
'input-streaming': 'hsl(217 91% 60%)', // blue
|
|
54
63
|
'input-available': 'hsl(38 92% 50%)', // amber
|
|
55
64
|
'output-available': 'hsl(142 71% 45%)', // green
|
|
@@ -57,8 +66,10 @@ const STATE_HUE: Record<ToolPart['state'], string> = {
|
|
|
57
66
|
};
|
|
58
67
|
|
|
59
68
|
function stateChip(state: ToolPart['state']): JSX.CSSProperties {
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
return {
|
|
70
|
+
color: STATE_TOKEN[state],
|
|
71
|
+
background: `color-mix(in oklab, ${STATE_FILL[state]} 15%, transparent)`,
|
|
72
|
+
};
|
|
62
73
|
}
|
|
63
74
|
|
|
64
75
|
function ToolStateBadge(props: { state: ToolPart['state'] }) {
|
|
@@ -16,6 +16,9 @@ export function VoiceInput(props: VoiceInputProps) {
|
|
|
16
16
|
const { isRecording, start, stop } = useVoiceRecorder();
|
|
17
17
|
const [isProcessing, setIsProcessing] = createSignal(false);
|
|
18
18
|
|
|
19
|
+
const label = () =>
|
|
20
|
+
isProcessing() ? 'Transcribing...' : isRecording() ? 'Stop recording' : 'Voice input';
|
|
21
|
+
|
|
19
22
|
async function handleClick() {
|
|
20
23
|
if (isRecording()) {
|
|
21
24
|
stop();
|
|
@@ -76,10 +79,11 @@ export function VoiceInput(props: VoiceInputProps) {
|
|
|
76
79
|
</For>
|
|
77
80
|
</Show>
|
|
78
81
|
|
|
79
|
-
<Tooltip content={
|
|
82
|
+
<Tooltip content={label()}>
|
|
80
83
|
<Button
|
|
81
84
|
variant="ghost"
|
|
82
85
|
size="icon-sm"
|
|
86
|
+
aria-label={label()}
|
|
83
87
|
onClick={handleClick}
|
|
84
88
|
disabled={local.disabled || isProcessing()}
|
|
85
89
|
class={cn(
|