@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
|
@@ -72,7 +72,12 @@ function PromptInput(props: PromptInputProps) {
|
|
|
72
72
|
<div
|
|
73
73
|
onClick={handleClick}
|
|
74
74
|
class={cn(
|
|
75
|
+
// The inner textarea neutralizes its own ring (focus-visible:ring-0),
|
|
76
|
+
// so the FRAME owns the focus affordance: a blue ring whenever a
|
|
77
|
+
// control inside it is focused. Without this the composer had no
|
|
78
|
+
// visible keyboard-focus state.
|
|
75
79
|
'bg-muted/40 cursor-text rounded-xl p-2 shadow-xs',
|
|
80
|
+
'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-0',
|
|
76
81
|
local.disabled && 'cursor-not-allowed opacity-60',
|
|
77
82
|
local.class
|
|
78
83
|
)}
|
|
@@ -121,8 +126,21 @@ function PromptInputTextarea(props: PromptInputTextareaProps) {
|
|
|
121
126
|
));
|
|
122
127
|
|
|
123
128
|
function handleInput(e: InputEvent & { currentTarget: HTMLTextAreaElement }) {
|
|
124
|
-
|
|
125
|
-
|
|
129
|
+
const el = e.currentTarget;
|
|
130
|
+
let value = el.value;
|
|
131
|
+
// Disallow leading whitespace — a prompt can't start with a space or blank
|
|
132
|
+
// line. Strip it (covers typing a space at the start AND pasting) and keep
|
|
133
|
+
// the caret in the right place.
|
|
134
|
+
if (/^\s/.test(value)) {
|
|
135
|
+
const stripped = value.replace(/^\s+/, '');
|
|
136
|
+
const removed = value.length - stripped.length;
|
|
137
|
+
const caret = Math.max(0, (el.selectionStart ?? 0) - removed);
|
|
138
|
+
el.value = stripped;
|
|
139
|
+
el.setSelectionRange(caret, caret);
|
|
140
|
+
value = stripped;
|
|
141
|
+
}
|
|
142
|
+
adjustHeight(el);
|
|
143
|
+
ctx.setValue(value);
|
|
126
144
|
}
|
|
127
145
|
|
|
128
146
|
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(
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js';
|
|
2
|
+
import { defineKitnElement } from './define';
|
|
3
|
+
import {
|
|
4
|
+
Attachments,
|
|
5
|
+
Attachment,
|
|
6
|
+
AttachmentPreview,
|
|
7
|
+
AttachmentInfo,
|
|
8
|
+
AttachmentRemove,
|
|
9
|
+
AttachmentHoverCard,
|
|
10
|
+
AttachmentHoverCardTrigger,
|
|
11
|
+
AttachmentHoverCardContent,
|
|
12
|
+
AttachmentEmpty,
|
|
13
|
+
getAttachmentLabel,
|
|
14
|
+
getMediaCategory,
|
|
15
|
+
type AttachmentData,
|
|
16
|
+
type AttachmentVariant,
|
|
17
|
+
} from '../components/attachments';
|
|
18
|
+
|
|
19
|
+
interface Props extends Record<string, unknown> {
|
|
20
|
+
/** The attachments to render. Set as a JS property (array). */
|
|
21
|
+
items: AttachmentData[];
|
|
22
|
+
/** Layout: `grid` = visual tiles, `inline` = icon + label chips, `list` = rows. */
|
|
23
|
+
variant?: AttachmentVariant;
|
|
24
|
+
/** Wrap each item in a hover card that previews its details. */
|
|
25
|
+
hoverCard?: boolean;
|
|
26
|
+
/** Show a remove button per item; clicking it fires a `remove` event. */
|
|
27
|
+
removable?: boolean;
|
|
28
|
+
/** Also show the media type beneath the filename (non-grid variants). */
|
|
29
|
+
showMediaType?: boolean;
|
|
30
|
+
/** Text shown when `items` is empty. */
|
|
31
|
+
emptyText?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Events fired by `<kitn-attachments>`. */
|
|
35
|
+
interface Events {
|
|
36
|
+
/** A remove button was clicked. */
|
|
37
|
+
remove: { id: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `<kitn-attachments>` — the exemplar for the "collapse a compound primitive to
|
|
42
|
+
* ONE configurable element" pattern (Route 1). The presentation knobs that the
|
|
43
|
+
* SolidJS layer expresses by composing sub-parts (`<AttachmentPreview>`,
|
|
44
|
+
* `<AttachmentInfo>`, `<AttachmentHoverCard>`, `<AttachmentRemove>`) become
|
|
45
|
+
* attributes/flags here:
|
|
46
|
+
*
|
|
47
|
+
* - icon + label .......... `variant="inline"`
|
|
48
|
+
* - visual + hover card .... `variant="grid" hover-card`
|
|
49
|
+
* - removable chips ........ add `removable` (emits `remove` → { id })
|
|
50
|
+
*
|
|
51
|
+
* Data in via the `items` property; the only interaction (`remove`) comes back
|
|
52
|
+
* as an event. For fully-custom hover content, the SolidJS primitives remain the
|
|
53
|
+
* escape hatch (a templated slot — "Route 2" — is a deliberate future add).
|
|
54
|
+
*/
|
|
55
|
+
defineKitnElement<Props, Events>('kitn-attachments', {
|
|
56
|
+
items: [],
|
|
57
|
+
variant: 'grid',
|
|
58
|
+
hoverCard: false,
|
|
59
|
+
removable: false,
|
|
60
|
+
showMediaType: false,
|
|
61
|
+
emptyText: undefined,
|
|
62
|
+
}, (props, { dispatch, flag }) => {
|
|
63
|
+
const variant = () => props.variant ?? 'grid';
|
|
64
|
+
const hoverCard = () => flag('hoverCard');
|
|
65
|
+
const removable = () => flag('removable');
|
|
66
|
+
const showMediaType = () => flag('showMediaType');
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Show
|
|
70
|
+
when={props.items.length}
|
|
71
|
+
fallback={<Show when={props.emptyText}><AttachmentEmpty>{props.emptyText}</AttachmentEmpty></Show>}
|
|
72
|
+
>
|
|
73
|
+
<Attachments variant={variant()}>
|
|
74
|
+
<For each={props.items}>
|
|
75
|
+
{(item) => (
|
|
76
|
+
<Attachment
|
|
77
|
+
data={item}
|
|
78
|
+
onRemove={removable() ? () => dispatch('remove', { id: item.id }) : undefined}
|
|
79
|
+
>
|
|
80
|
+
<Show
|
|
81
|
+
when={hoverCard() && variant() !== 'grid'}
|
|
82
|
+
fallback={
|
|
83
|
+
<>
|
|
84
|
+
<AttachmentPreview />
|
|
85
|
+
{/* Info only for non-grid; grid is a self-contained visual tile. */}
|
|
86
|
+
<Show when={variant() !== 'grid'}>
|
|
87
|
+
<AttachmentInfo showMediaType={showMediaType()} />
|
|
88
|
+
</Show>
|
|
89
|
+
</>
|
|
90
|
+
}
|
|
91
|
+
>
|
|
92
|
+
{/* Hover preview is for compact inline/list chips; grid tiles are
|
|
93
|
+
already the visual, so they skip it (wrapping them would also
|
|
94
|
+
collapse non-image tiles to the icon's height). */}
|
|
95
|
+
<AttachmentHoverCard>
|
|
96
|
+
<AttachmentHoverCardTrigger>
|
|
97
|
+
<div class="flex items-center gap-1.5">
|
|
98
|
+
<AttachmentPreview />
|
|
99
|
+
<AttachmentInfo showMediaType={showMediaType()} />
|
|
100
|
+
</div>
|
|
101
|
+
</AttachmentHoverCardTrigger>
|
|
102
|
+
<AttachmentHoverCardContent>
|
|
103
|
+
{/* For image attachments, preview the actual thumbnail;
|
|
104
|
+
otherwise fall back to the label + media-type details. */}
|
|
105
|
+
<Show
|
|
106
|
+
when={getMediaCategory(item) === 'image' && item.type === 'file' && item.url}
|
|
107
|
+
fallback={
|
|
108
|
+
<>
|
|
109
|
+
<div class="text-body font-medium">{getAttachmentLabel(item)}</div>
|
|
110
|
+
<Show when={item.mediaType}>
|
|
111
|
+
<div class="text-muted-foreground text-caption">{item.mediaType}</div>
|
|
112
|
+
</Show>
|
|
113
|
+
</>
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<img
|
|
117
|
+
src={item.url}
|
|
118
|
+
alt={getAttachmentLabel(item)}
|
|
119
|
+
class="block max-h-64 max-w-xs rounded object-contain"
|
|
120
|
+
/>
|
|
121
|
+
</Show>
|
|
122
|
+
</AttachmentHoverCardContent>
|
|
123
|
+
</AttachmentHoverCard>
|
|
124
|
+
</Show>
|
|
125
|
+
<AttachmentRemove />
|
|
126
|
+
</Attachment>
|
|
127
|
+
)}
|
|
128
|
+
</For>
|
|
129
|
+
</Attachments>
|
|
130
|
+
</Show>
|
|
131
|
+
);
|
|
132
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { For, Show } from 'solid-js';
|
|
2
|
+
import { defineKitnElement } from './define';
|
|
3
|
+
import {
|
|
4
|
+
ChainOfThought,
|
|
5
|
+
ChainOfThoughtStep,
|
|
6
|
+
ChainOfThoughtTrigger,
|
|
7
|
+
ChainOfThoughtContent,
|
|
8
|
+
ChainOfThoughtItem,
|
|
9
|
+
} from '../components/chain-of-thought';
|
|
10
|
+
|
|
11
|
+
interface Step {
|
|
12
|
+
/** The step's heading (the always-visible trigger). */
|
|
13
|
+
label: string;
|
|
14
|
+
/** Optional expandable detail. */
|
|
15
|
+
content?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props extends Record<string, unknown> {
|
|
19
|
+
/** The reasoning steps. Set as a JS property. Compound sub-parts collapse to
|
|
20
|
+
* this one data model (Route 1). */
|
|
21
|
+
steps: Step[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `<kitn-chain-of-thought>` — step-by-step reasoning with connectors and
|
|
26
|
+
* per-step collapsible detail. Data via the `steps` property.
|
|
27
|
+
*/
|
|
28
|
+
defineKitnElement<Props>('kitn-chain-of-thought', {
|
|
29
|
+
steps: [],
|
|
30
|
+
}, (props) => (
|
|
31
|
+
<ChainOfThought>
|
|
32
|
+
<For each={props.steps}>
|
|
33
|
+
{(step, i) => (
|
|
34
|
+
<ChainOfThoughtStep isLast={i() === props.steps.length - 1}>
|
|
35
|
+
<ChainOfThoughtTrigger>{step.label}</ChainOfThoughtTrigger>
|
|
36
|
+
<Show when={step.content}>
|
|
37
|
+
<ChainOfThoughtContent>
|
|
38
|
+
<ChainOfThoughtItem>{step.content}</ChainOfThoughtItem>
|
|
39
|
+
</ChainOfThoughtContent>
|
|
40
|
+
</Show>
|
|
41
|
+
</ChainOfThoughtStep>
|
|
42
|
+
)}
|
|
43
|
+
</For>
|
|
44
|
+
</ChainOfThought>
|
|
45
|
+
));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { defineKitnElement } from './define';
|
|
2
|
+
import { ChatScopePicker } from '../components/chat-scope-picker';
|
|
3
|
+
import type { SearchFilters } from '../types';
|
|
4
|
+
|
|
5
|
+
interface Props extends Record<string, unknown> {
|
|
6
|
+
/** Authors to offer as scope filters. Set as a JS property. */
|
|
7
|
+
availableAuthors: string[];
|
|
8
|
+
/** Tags to offer as scope filters. Set as a JS property. */
|
|
9
|
+
availableTags: string[];
|
|
10
|
+
/** The label shown on the trigger for the active scope. */
|
|
11
|
+
currentLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Events fired by `<kitn-chat-scope-picker>`. */
|
|
15
|
+
interface Events {
|
|
16
|
+
/** A scope was chosen (`undefined` filters = "All Content"). */
|
|
17
|
+
scopechange: { filters: SearchFilters | undefined };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `<kitn-chat-scope-picker>` — a dropdown to scope a chat by author or tag.
|
|
22
|
+
* Options via `available-authors`/`available-tags` properties; emits
|
|
23
|
+
* `scopechange`.
|
|
24
|
+
*/
|
|
25
|
+
defineKitnElement<Props, Events>('kitn-chat-scope-picker', {
|
|
26
|
+
availableAuthors: [],
|
|
27
|
+
availableTags: [],
|
|
28
|
+
currentLabel: 'All Content',
|
|
29
|
+
}, (props, { dispatch }) => (
|
|
30
|
+
<ChatScopePicker
|
|
31
|
+
currentLabel={props.currentLabel ?? 'All Content'}
|
|
32
|
+
availableAuthors={props.availableAuthors}
|
|
33
|
+
availableTags={props.availableTags}
|
|
34
|
+
onScopeChange={(filters) => dispatch('scopechange', { filters })}
|
|
35
|
+
/>
|
|
36
|
+
));
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createSignal, Show } from 'solid-js';
|
|
2
|
+
import { defineKitnElement } from './define';
|
|
3
|
+
import { ChatThread, type ChatThreadContextUsage } from '../components/chat-thread';
|
|
4
|
+
import { ConversationList } from '../components/conversation-list';
|
|
5
|
+
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '../ui/resizable';
|
|
6
|
+
import { Button } from '../ui/button';
|
|
7
|
+
import { PanelLeftOpen } from 'lucide-solid';
|
|
8
|
+
import type { SlashCommandItem } from '../components/slash-command';
|
|
9
|
+
import type { ChatMessage } from './chat-types';
|
|
10
|
+
import type { ProseSize } from '../primitives/chat-config';
|
|
11
|
+
import type { ModelOption, ConversationGroup, ConversationSummary } from '../types';
|
|
12
|
+
|
|
13
|
+
interface Props extends Record<string, unknown> {
|
|
14
|
+
/** Pre-bucketed conversation groups for the sidebar. Set as a JS property. */
|
|
15
|
+
groups: ConversationGroup[];
|
|
16
|
+
/** Flat conversation list (auto-bucketed if `groups` is empty). Set as a JS property. */
|
|
17
|
+
conversations: ConversationSummary[];
|
|
18
|
+
/** Id of the open conversation, highlighted in the sidebar. */
|
|
19
|
+
activeId?: string;
|
|
20
|
+
/** The active conversation's message thread, newest last. Set as a JS property. */
|
|
21
|
+
messages: ChatMessage[];
|
|
22
|
+
value?: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
loading?: boolean;
|
|
25
|
+
suggestions?: string[];
|
|
26
|
+
suggestionMode?: 'submit' | 'fill';
|
|
27
|
+
proseSize?: ProseSize;
|
|
28
|
+
codeTheme?: string;
|
|
29
|
+
codeHighlight?: boolean;
|
|
30
|
+
chatTitle?: string;
|
|
31
|
+
models?: ModelOption[];
|
|
32
|
+
currentModel?: string;
|
|
33
|
+
context?: ChatThreadContextUsage;
|
|
34
|
+
scrollButton?: boolean;
|
|
35
|
+
search?: boolean;
|
|
36
|
+
voice?: boolean;
|
|
37
|
+
slashCommands?: SlashCommandItem[];
|
|
38
|
+
slashActiveIds?: string[];
|
|
39
|
+
slashCompact?: boolean;
|
|
40
|
+
/** Sidebar default width as a percent of the workspace (default 22). */
|
|
41
|
+
sidebarWidth?: number;
|
|
42
|
+
/** Sidebar min width in px (default 200). */
|
|
43
|
+
sidebarMinWidth?: number;
|
|
44
|
+
/** Sidebar max width in px (default 420). */
|
|
45
|
+
sidebarMaxWidth?: number;
|
|
46
|
+
/** Initial collapsed state of the sidebar (default false). */
|
|
47
|
+
sidebarCollapsed?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
defineKitnElement<Props>('kitn-chat-workspace', {
|
|
51
|
+
groups: [], conversations: [], activeId: undefined, messages: [],
|
|
52
|
+
value: undefined, placeholder: 'Send a message...', loading: false,
|
|
53
|
+
suggestions: undefined, suggestionMode: 'submit', proseSize: 'sm',
|
|
54
|
+
codeTheme: 'github-dark-dimmed', codeHighlight: true, chatTitle: undefined,
|
|
55
|
+
models: undefined, currentModel: undefined, context: undefined, scrollButton: true,
|
|
56
|
+
search: false, voice: false, slashCommands: undefined, slashActiveIds: undefined, slashCompact: false,
|
|
57
|
+
sidebarWidth: 22, sidebarMinWidth: 200, sidebarMaxWidth: 420, sidebarCollapsed: false,
|
|
58
|
+
}, (props, { dispatch, flag }) => {
|
|
59
|
+
// Collapse is internal UI state; `sidebarCollapsed` only sets the initial value
|
|
60
|
+
// (not a controlled binding).
|
|
61
|
+
const [collapsed, setCollapsed] = createSignal(props.sidebarCollapsed === true);
|
|
62
|
+
const toggle = () => { const next = !collapsed(); setCollapsed(next); dispatch('sidebartoggle', { collapsed: next }); };
|
|
63
|
+
|
|
64
|
+
// Create the thread ONCE and reference the same node in both <Show> branches.
|
|
65
|
+
// It's owned by this component root (not by a Show branch), so toggling the
|
|
66
|
+
// sidebar moves the node between branches without disposing it — the thread's
|
|
67
|
+
// own state (e.g. an uncontrolled draft) survives the collapse/expand.
|
|
68
|
+
const threadEl = (
|
|
69
|
+
<ChatThread
|
|
70
|
+
messages={props.messages} value={props.value as string | undefined} placeholder={props.placeholder as string}
|
|
71
|
+
loading={flag('loading')} suggestions={props.suggestions as string[] | undefined}
|
|
72
|
+
suggestionMode={props.suggestionMode as 'submit' | 'fill'} proseSize={props.proseSize as ProseSize}
|
|
73
|
+
codeTheme={props.codeTheme as string} codeHighlight={flag('codeHighlight')}
|
|
74
|
+
chatTitle={props.chatTitle as string | undefined} models={props.models as ModelOption[] | undefined}
|
|
75
|
+
currentModel={props.currentModel as string | undefined} context={props.context as ChatThreadContextUsage | undefined}
|
|
76
|
+
scrollButton={props.scrollButton !== false} search={flag('search')} voice={flag('voice')}
|
|
77
|
+
slashCommands={props.slashCommands as SlashCommandItem[] | undefined}
|
|
78
|
+
slashActiveIds={props.slashActiveIds as string[] | undefined} slashCompact={flag('slashCompact')}
|
|
79
|
+
onValueChange={(value) => dispatch('valuechange', { value })}
|
|
80
|
+
onSubmit={(detail) => dispatch('submit', detail)}
|
|
81
|
+
onSuggestionClick={(value) => dispatch('suggestionclick', { value })}
|
|
82
|
+
onModelChange={(modelId) => dispatch('modelchange', { modelId })}
|
|
83
|
+
onMessageAction={(detail) => dispatch('messageaction', detail)}
|
|
84
|
+
onSearch={() => dispatch('search', {})}
|
|
85
|
+
onVoice={() => dispatch('voice', {})}
|
|
86
|
+
onSlashSelect={(command) => dispatch('slashselect', { command })}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div class="h-full w-full overflow-hidden bg-background">
|
|
92
|
+
<Show
|
|
93
|
+
when={!collapsed()}
|
|
94
|
+
fallback={
|
|
95
|
+
<div class="relative h-full">
|
|
96
|
+
<Button
|
|
97
|
+
variant="ghost" size="icon-sm" aria-label="Open sidebar"
|
|
98
|
+
class="absolute left-2 top-2 z-10 rounded-full bg-card/80 shadow-sm backdrop-blur"
|
|
99
|
+
onClick={toggle}
|
|
100
|
+
>
|
|
101
|
+
<PanelLeftOpen class="size-4" />
|
|
102
|
+
</Button>
|
|
103
|
+
{threadEl}
|
|
104
|
+
</div>
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
<ResizablePanelGroup orientation="horizontal">
|
|
108
|
+
<ResizablePanel defaultSize={props.sidebarWidth as number} data-min-size={String(props.sidebarMinWidth)} data-max-size={String(props.sidebarMaxWidth)}>
|
|
109
|
+
<ConversationList
|
|
110
|
+
groups={props.groups} conversations={props.conversations} activeId={props.activeId as string | undefined}
|
|
111
|
+
onSelect={(id) => dispatch('conversationselect', { id })}
|
|
112
|
+
onNewChat={() => dispatch('newchat', {})}
|
|
113
|
+
onToggleSidebar={toggle}
|
|
114
|
+
/>
|
|
115
|
+
</ResizablePanel>
|
|
116
|
+
<ResizableHandle withHandle />
|
|
117
|
+
<ResizablePanel>{threadEl}</ResizablePanel>
|
|
118
|
+
</ResizablePanelGroup>
|
|
119
|
+
</Show>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
});
|