@kitnai/chat 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +314 -0
- package/dist/bash-InADTalH.js +6 -0
- package/dist/core-AYMC6_lb.js +5874 -0
- package/dist/engine-javascript-vq0WuIJl.js +2643 -0
- package/dist/github-dark-dimmed-DUshB20C.js +4 -0
- package/dist/github-light-JYsPkUQd.js +4 -0
- package/dist/javascript-C25yR2R2.js +6 -0
- package/dist/json-DxJze_jm.js +6 -0
- package/dist/kitn-chat.es.js +6632 -0
- package/dist/tsx-B8rCNbgL.js +6 -0
- package/dist/typescript-RycA9KXf.js +6 -0
- package/package.json +80 -0
- package/src/components/attachments.stories.tsx +304 -0
- package/src/components/attachments.tsx +394 -0
- package/src/components/chain-of-thought.stories.tsx +212 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +188 -0
- package/src/components/chat-container.tsx +78 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/checkpoint.stories.tsx +103 -0
- package/src/components/checkpoint.tsx +81 -0
- package/src/components/code-block.stories.tsx +151 -0
- package/src/components/code-block.tsx +99 -0
- package/src/components/context.stories.tsx +180 -0
- package/src/components/context.tsx +323 -0
- package/src/components/conversation-item.stories.tsx +126 -0
- package/src/components/conversation-item.tsx +18 -0
- package/src/components/conversation-list.stories.tsx +134 -0
- package/src/components/conversation-list.tsx +100 -0
- package/src/components/empty.stories.tsx +435 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +101 -0
- package/src/components/feedback-bar.tsx +58 -0
- package/src/components/file-upload.stories.tsx +157 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/image.stories.tsx +90 -0
- package/src/components/image.tsx +67 -0
- package/src/components/loader.stories.tsx +182 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +181 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-narrow.stories.tsx +330 -0
- package/src/components/message-skills.stories.tsx +212 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +282 -0
- package/src/components/message.tsx +149 -0
- package/src/components/model-switcher.stories.tsx +98 -0
- package/src/components/model-switcher.tsx +36 -0
- package/src/components/prompt-input.stories.tsx +223 -0
- package/src/components/prompt-input.tsx +190 -0
- package/src/components/prompt-suggestion.stories.tsx +143 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +141 -0
- package/src/components/reasoning.tsx +157 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +101 -0
- package/src/components/scroll-button.tsx +33 -0
- package/src/components/slash-command.stories.tsx +164 -0
- package/src/components/slash-command.tsx +223 -0
- package/src/components/source.stories.tsx +125 -0
- package/src/components/source.tsx +129 -0
- package/src/components/text-shimmer.stories.tsx +88 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +88 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +154 -0
- package/src/components/tool.tsx +173 -0
- package/src/components/voice-input.stories.tsx +84 -0
- package/src/components/voice-input.tsx +103 -0
- package/src/elements/chat-types.ts +14 -0
- package/src/elements/chat.tsx +111 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/conversation-list.tsx +26 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +53 -0
- package/src/elements/define.tsx +54 -0
- package/src/elements/kitn-chat.stories.tsx +105 -0
- package/src/elements/kitn-conversation-list.stories.tsx +177 -0
- package/src/elements/kitn-prompt-input.stories.tsx +123 -0
- package/src/elements/prompt-input.tsx +39 -0
- package/src/elements/register.ts +9 -0
- package/src/elements/styles.css +12 -0
- package/src/index.ts +128 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/highlighter.ts +150 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-stick-to-bottom.ts +43 -0
- package/src/primitives/use-text-stream.ts +112 -0
- package/src/primitives/use-voice-recorder.ts +50 -0
- package/src/stories/chat-panel-layout.stories.tsx +144 -0
- package/src/stories/chat-scene.tsx +570 -0
- package/src/stories/checkpoint-restore.stories.tsx +224 -0
- package/src/stories/context-usage.stories.tsx +155 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +151 -0
- package/src/stories/conversation-with-sources.stories.tsx +165 -0
- package/src/stories/docs/GettingStarted.mdx +76 -0
- package/src/stories/docs/Installation.mdx +48 -0
- package/src/stories/docs/Integrations.mdx +110 -0
- package/src/stories/docs/Introduction.mdx +29 -0
- package/src/stories/docs/Theming.mdx +87 -0
- package/src/stories/docs/theme-editor/canvas.tsx +32 -0
- package/src/stories/docs/theme-editor/inspector.tsx +66 -0
- package/src/stories/docs/theme-editor/presets.test.ts +32 -0
- package/src/stories/docs/theme-editor/presets.ts +64 -0
- package/src/stories/docs/theme-editor/theme-css.test.ts +19 -0
- package/src/stories/docs/theme-editor/theme-css.ts +15 -0
- package/src/stories/docs/theme-editor/theme-editor.tsx +145 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +167 -0
- package/src/stories/prompt-input-variants.stories.tsx +179 -0
- package/src/stories/streaming-response.stories.tsx +234 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/types.ts +41 -0
- package/src/ui/avatar.stories.tsx +104 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +87 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +146 -0
- package/src/ui/button.tsx +37 -0
- package/src/ui/collapsible.tsx +14 -0
- package/src/ui/dialog.tsx +21 -0
- package/src/ui/dropdown.tsx +26 -0
- package/src/ui/hover-card.tsx +48 -0
- package/src/ui/resizable.stories.tsx +171 -0
- package/src/ui/resizable.tsx +219 -0
- package/src/ui/scroll-area.tsx +13 -0
- package/src/ui/separator.stories.tsx +82 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +338 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +75 -0
- package/src/ui/tooltip.tsx +22 -0
- package/src/utils/cn.ts +6 -0
- package/theme.css +115 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { type JSX, Show, createContext, createMemo, splitProps, useContext } from 'solid-js';
|
|
2
|
+
import { HoverCard as KHoverCard } from '@kobalte/core/hover-card';
|
|
3
|
+
import { cn } from '../utils/cn';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { useChatConfig } from '../primitives/chat-config';
|
|
6
|
+
|
|
7
|
+
const ICON_RADIUS = 10;
|
|
8
|
+
const ICON_VIEWBOX = 24;
|
|
9
|
+
const ICON_CENTER = 12;
|
|
10
|
+
const ICON_STROKE_WIDTH = 2;
|
|
11
|
+
const PERCENT_MAX = 100;
|
|
12
|
+
|
|
13
|
+
interface ContextSchema {
|
|
14
|
+
usedTokens: number;
|
|
15
|
+
maxTokens: number;
|
|
16
|
+
inputTokens?: number;
|
|
17
|
+
outputTokens?: number;
|
|
18
|
+
reasoningTokens?: number;
|
|
19
|
+
cacheTokens?: number;
|
|
20
|
+
estimatedCost?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ContextCtx = createContext<ContextSchema>();
|
|
24
|
+
|
|
25
|
+
function useContextValue(): ContextSchema {
|
|
26
|
+
const ctx = useContext(ContextCtx);
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new Error('Context components must be used within Context');
|
|
29
|
+
}
|
|
30
|
+
return ctx;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const fmtCompact = new Intl.NumberFormat('en-US', { notation: 'compact' });
|
|
34
|
+
const fmtPercent = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, style: 'percent' });
|
|
35
|
+
const fmtCurrency = new Intl.NumberFormat('en-US', { currency: 'USD', style: 'currency' });
|
|
36
|
+
|
|
37
|
+
// --- Root provider ---
|
|
38
|
+
|
|
39
|
+
export interface ContextProps {
|
|
40
|
+
usedTokens: number;
|
|
41
|
+
maxTokens: number;
|
|
42
|
+
inputTokens?: number;
|
|
43
|
+
outputTokens?: number;
|
|
44
|
+
reasoningTokens?: number;
|
|
45
|
+
cacheTokens?: number;
|
|
46
|
+
estimatedCost?: number;
|
|
47
|
+
children?: JSX.Element;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function Context(props: ContextProps) {
|
|
51
|
+
const value = createMemo<ContextSchema>(() => ({
|
|
52
|
+
usedTokens: props.usedTokens,
|
|
53
|
+
maxTokens: props.maxTokens,
|
|
54
|
+
inputTokens: props.inputTokens,
|
|
55
|
+
outputTokens: props.outputTokens,
|
|
56
|
+
reasoningTokens: props.reasoningTokens,
|
|
57
|
+
cacheTokens: props.cacheTokens,
|
|
58
|
+
estimatedCost: props.estimatedCost,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<ContextCtx.Provider value={value()}>
|
|
63
|
+
<KHoverCard openDelay={0} closeDelay={0}>
|
|
64
|
+
{props.children}
|
|
65
|
+
</KHoverCard>
|
|
66
|
+
</ContextCtx.Provider>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --- Icon (internal) ---
|
|
71
|
+
|
|
72
|
+
function ContextIcon() {
|
|
73
|
+
const ctx = useContextValue();
|
|
74
|
+
const circumference = 2 * Math.PI * ICON_RADIUS;
|
|
75
|
+
const usedPercent = createMemo(() => ctx.usedTokens / ctx.maxTokens);
|
|
76
|
+
const dashOffset = createMemo(() => circumference * (1 - usedPercent()));
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<svg
|
|
80
|
+
aria-label="Model context usage"
|
|
81
|
+
height="20"
|
|
82
|
+
role="img"
|
|
83
|
+
style={{ color: 'currentcolor' }}
|
|
84
|
+
viewBox={`0 0 ${ICON_VIEWBOX} ${ICON_VIEWBOX}`}
|
|
85
|
+
width="20"
|
|
86
|
+
>
|
|
87
|
+
<circle
|
|
88
|
+
cx={ICON_CENTER}
|
|
89
|
+
cy={ICON_CENTER}
|
|
90
|
+
fill="none"
|
|
91
|
+
opacity="0.25"
|
|
92
|
+
r={ICON_RADIUS}
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
stroke-width={ICON_STROKE_WIDTH}
|
|
95
|
+
/>
|
|
96
|
+
<circle
|
|
97
|
+
cx={ICON_CENTER}
|
|
98
|
+
cy={ICON_CENTER}
|
|
99
|
+
fill="none"
|
|
100
|
+
opacity="0.7"
|
|
101
|
+
r={ICON_RADIUS}
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
stroke-dasharray={`${circumference} ${circumference}`}
|
|
104
|
+
stroke-dashoffset={dashOffset()}
|
|
105
|
+
stroke-linecap="round"
|
|
106
|
+
stroke-width={ICON_STROKE_WIDTH}
|
|
107
|
+
style={{ transform: 'rotate(-90deg)', 'transform-origin': 'center' }}
|
|
108
|
+
/>
|
|
109
|
+
</svg>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// --- Trigger ---
|
|
114
|
+
|
|
115
|
+
export interface ContextTriggerProps {
|
|
116
|
+
children?: JSX.Element;
|
|
117
|
+
class?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function ContextTrigger(props: ContextTriggerProps) {
|
|
121
|
+
const ctx = useContextValue();
|
|
122
|
+
const usedPercent = createMemo(() => ctx.usedTokens / ctx.maxTokens);
|
|
123
|
+
const renderedPercent = createMemo(() => fmtPercent.format(usedPercent()));
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<KHoverCard.Trigger as="span">
|
|
127
|
+
<Show
|
|
128
|
+
when={!props.children}
|
|
129
|
+
fallback={props.children}
|
|
130
|
+
>
|
|
131
|
+
<Button type="button" variant="ghost" class={props.class}>
|
|
132
|
+
<span class="font-medium text-muted-foreground">{renderedPercent()}</span>
|
|
133
|
+
<ContextIcon />
|
|
134
|
+
</Button>
|
|
135
|
+
</Show>
|
|
136
|
+
</KHoverCard.Trigger>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Content ---
|
|
141
|
+
|
|
142
|
+
export interface ContextContentProps {
|
|
143
|
+
class?: string;
|
|
144
|
+
children?: JSX.Element;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function ContextContent(props: ContextContentProps) {
|
|
148
|
+
const config = useChatConfig();
|
|
149
|
+
return (
|
|
150
|
+
<KHoverCard.Portal mount={config.portalMount()}>
|
|
151
|
+
<KHoverCard.Content
|
|
152
|
+
class={cn(
|
|
153
|
+
'z-50 min-w-60 divide-y divide-border overflow-hidden rounded-lg bg-card shadow-lg animate-in fade-in-0 zoom-in-95',
|
|
154
|
+
props.class
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{props.children}
|
|
158
|
+
</KHoverCard.Content>
|
|
159
|
+
</KHoverCard.Portal>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Content Header ---
|
|
164
|
+
|
|
165
|
+
export interface ContextContentHeaderProps {
|
|
166
|
+
class?: string;
|
|
167
|
+
children?: JSX.Element;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function ContextContentHeader(props: ContextContentHeaderProps) {
|
|
171
|
+
const ctx = useContextValue();
|
|
172
|
+
const usedPercent = createMemo(() => ctx.usedTokens / ctx.maxTokens);
|
|
173
|
+
const displayPct = createMemo(() => fmtPercent.format(usedPercent()));
|
|
174
|
+
const used = createMemo(() => fmtCompact.format(ctx.usedTokens));
|
|
175
|
+
const total = createMemo(() => fmtCompact.format(ctx.maxTokens));
|
|
176
|
+
const barWidth = createMemo(() => `${Math.min(usedPercent() * PERCENT_MAX, PERCENT_MAX)}%`);
|
|
177
|
+
|
|
178
|
+
const colorClass = createMemo(() => {
|
|
179
|
+
const pct = usedPercent() * PERCENT_MAX;
|
|
180
|
+
if (pct > 90) return 'bg-red-400';
|
|
181
|
+
if (pct > 70) return 'bg-yellow-400';
|
|
182
|
+
return 'bg-primary';
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<div class={cn('w-full space-y-2 p-3', props.class)}>
|
|
187
|
+
<Show when={!props.children} fallback={props.children}>
|
|
188
|
+
<div class="flex items-center justify-between gap-3 text-xs">
|
|
189
|
+
<p>{displayPct()}</p>
|
|
190
|
+
<p class="font-mono text-muted-foreground">
|
|
191
|
+
{used()} / {total()}
|
|
192
|
+
</p>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="space-y-2">
|
|
195
|
+
<div class="h-1.5 w-full rounded-full bg-muted overflow-hidden">
|
|
196
|
+
<div
|
|
197
|
+
class={cn('h-full rounded-full transition-all', colorClass())}
|
|
198
|
+
style={{ width: barWidth() }}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
</Show>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Content Body ---
|
|
208
|
+
|
|
209
|
+
export interface ContextContentBodyProps {
|
|
210
|
+
class?: string;
|
|
211
|
+
children?: JSX.Element;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function ContextContentBody(props: ContextContentBodyProps) {
|
|
215
|
+
return (
|
|
216
|
+
<div class={cn('w-full p-3', props.class)}>
|
|
217
|
+
{props.children}
|
|
218
|
+
</div>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// --- Content Footer ---
|
|
223
|
+
|
|
224
|
+
export interface ContextContentFooterProps {
|
|
225
|
+
class?: string;
|
|
226
|
+
children?: JSX.Element;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function ContextContentFooter(props: ContextContentFooterProps) {
|
|
230
|
+
const ctx = useContextValue();
|
|
231
|
+
const totalCost = createMemo(() =>
|
|
232
|
+
fmtCurrency.format(ctx.estimatedCost ?? 0)
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div
|
|
237
|
+
class={cn(
|
|
238
|
+
'flex w-full items-center justify-between gap-3 bg-muted p-3 text-xs',
|
|
239
|
+
props.class
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
<Show when={!props.children} fallback={props.children}>
|
|
243
|
+
<span class="text-muted-foreground">Total cost</span>
|
|
244
|
+
<span>{totalCost()}</span>
|
|
245
|
+
</Show>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- Token row helper ---
|
|
251
|
+
|
|
252
|
+
function TokensDisplay(props: { tokens?: number }) {
|
|
253
|
+
return (
|
|
254
|
+
<span>
|
|
255
|
+
{props.tokens === undefined
|
|
256
|
+
? '\u2014'
|
|
257
|
+
: fmtCompact.format(props.tokens)}
|
|
258
|
+
</span>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Specialized usage rows ---
|
|
263
|
+
|
|
264
|
+
export interface ContextUsageRowProps {
|
|
265
|
+
class?: string;
|
|
266
|
+
children?: JSX.Element;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function ContextInputUsage(props: ContextUsageRowProps) {
|
|
270
|
+
const ctx = useContextValue();
|
|
271
|
+
return (
|
|
272
|
+
<Show when={props.children || ctx.inputTokens}>
|
|
273
|
+
<Show when={!props.children} fallback={props.children}>
|
|
274
|
+
<div class={cn('flex items-center justify-between text-xs', props.class)}>
|
|
275
|
+
<span class="text-muted-foreground">Input</span>
|
|
276
|
+
<TokensDisplay tokens={ctx.inputTokens} />
|
|
277
|
+
</div>
|
|
278
|
+
</Show>
|
|
279
|
+
</Show>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function ContextOutputUsage(props: ContextUsageRowProps) {
|
|
284
|
+
const ctx = useContextValue();
|
|
285
|
+
return (
|
|
286
|
+
<Show when={props.children || ctx.outputTokens}>
|
|
287
|
+
<Show when={!props.children} fallback={props.children}>
|
|
288
|
+
<div class={cn('flex items-center justify-between text-xs', props.class)}>
|
|
289
|
+
<span class="text-muted-foreground">Output</span>
|
|
290
|
+
<TokensDisplay tokens={ctx.outputTokens} />
|
|
291
|
+
</div>
|
|
292
|
+
</Show>
|
|
293
|
+
</Show>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function ContextReasoningUsage(props: ContextUsageRowProps) {
|
|
298
|
+
const ctx = useContextValue();
|
|
299
|
+
return (
|
|
300
|
+
<Show when={props.children || ctx.reasoningTokens}>
|
|
301
|
+
<Show when={!props.children} fallback={props.children}>
|
|
302
|
+
<div class={cn('flex items-center justify-between text-xs', props.class)}>
|
|
303
|
+
<span class="text-muted-foreground">Reasoning</span>
|
|
304
|
+
<TokensDisplay tokens={ctx.reasoningTokens} />
|
|
305
|
+
</div>
|
|
306
|
+
</Show>
|
|
307
|
+
</Show>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function ContextCacheUsage(props: ContextUsageRowProps) {
|
|
312
|
+
const ctx = useContextValue();
|
|
313
|
+
return (
|
|
314
|
+
<Show when={props.children || ctx.cacheTokens}>
|
|
315
|
+
<Show when={!props.children} fallback={props.children}>
|
|
316
|
+
<div class={cn('flex items-center justify-between text-xs', props.class)}>
|
|
317
|
+
<span class="text-muted-foreground">Cache</span>
|
|
318
|
+
<TokensDisplay tokens={ctx.cacheTokens} />
|
|
319
|
+
</div>
|
|
320
|
+
</Show>
|
|
321
|
+
</Show>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
import { ConversationItem } from './conversation-item';
|
|
4
|
+
|
|
5
|
+
const baseConversation = {
|
|
6
|
+
id: '1',
|
|
7
|
+
title: 'How to use SolidJS signals',
|
|
8
|
+
messageCount: 8,
|
|
9
|
+
lastMessageAt: '2026-04-10T12:00:00Z',
|
|
10
|
+
updatedAt: '2026-04-10T12:00:00Z',
|
|
11
|
+
scope: { type: 'document' as const },
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A single selectable row in a conversation/chat list: title plus a message
|
|
16
|
+
* count, with an active (selected) state.
|
|
17
|
+
*/
|
|
18
|
+
const meta = {
|
|
19
|
+
title: 'Components/ConversationItem',
|
|
20
|
+
component: ConversationItem,
|
|
21
|
+
tags: ['autodocs'],
|
|
22
|
+
parameters: {
|
|
23
|
+
layout: 'padded',
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component: [
|
|
27
|
+
'A single conversation row — shows the title and message count, truncating long titles, with a highlighted active state.',
|
|
28
|
+
'**When to use:** as the leaf item inside a chat history sidebar. Usually rendered for you by `ConversationList`; use it directly to build a custom list.',
|
|
29
|
+
'**How to use:** pass a `conversation` summary, an `isActive` flag, and an `onSelect(id)` handler fired when the row is clicked.',
|
|
30
|
+
'**Placement:** inside a conversation/chat history sidebar list.',
|
|
31
|
+
].join('\n\n'),
|
|
32
|
+
},
|
|
33
|
+
controls: { exclude: ['use:eventListener'] },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
argTypes: {
|
|
37
|
+
conversation: {
|
|
38
|
+
control: 'object',
|
|
39
|
+
description: 'The conversation summary (id, title, messageCount, scope, timestamps).',
|
|
40
|
+
},
|
|
41
|
+
isActive: {
|
|
42
|
+
control: 'boolean',
|
|
43
|
+
description: 'Whether this row is the currently selected conversation.',
|
|
44
|
+
},
|
|
45
|
+
onSelect: {
|
|
46
|
+
action: 'select',
|
|
47
|
+
description: 'Fired with the conversation id when the row is clicked.',
|
|
48
|
+
table: { category: 'Events' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
args: {
|
|
52
|
+
conversation: baseConversation,
|
|
53
|
+
isActive: false,
|
|
54
|
+
onSelect: fn(),
|
|
55
|
+
},
|
|
56
|
+
render: (args) => (
|
|
57
|
+
<div class="w-64">
|
|
58
|
+
<ConversationItem {...args} />
|
|
59
|
+
</div>
|
|
60
|
+
),
|
|
61
|
+
} satisfies Meta<typeof ConversationItem>;
|
|
62
|
+
|
|
63
|
+
export default meta;
|
|
64
|
+
type Story = StoryObj<typeof meta>;
|
|
65
|
+
|
|
66
|
+
const IMPORT = `import { ConversationItem } from '@kitnai/chat';`;
|
|
67
|
+
const src = (code: string) => ({
|
|
68
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Interactive playground — toggle `isActive` and edit the conversation object. */
|
|
72
|
+
export const Playground: Story = {
|
|
73
|
+
...src(`<ConversationItem conversation={conversation} isActive={false} onSelect={(id) => {}} />`),
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const Active: Story = {
|
|
77
|
+
args: { isActive: true },
|
|
78
|
+
...src(`<ConversationItem conversation={conversation} isActive onSelect={(id) => {}} />`),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const Inactive: Story = {
|
|
82
|
+
args: { isActive: false },
|
|
83
|
+
...src(`<ConversationItem conversation={conversation} isActive={false} onSelect={(id) => {}} />`),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const LongTitle: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
conversation: {
|
|
89
|
+
...baseConversation,
|
|
90
|
+
title: 'This is a very long conversation title that should be truncated with an ellipsis',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
...src(`<ConversationItem
|
|
94
|
+
conversation={{ ...conversation, title: 'A very long title…' }}
|
|
95
|
+
isActive={false}
|
|
96
|
+
onSelect={(id) => {}}
|
|
97
|
+
/>`),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/** Several items stacked, one active — showcase. */
|
|
101
|
+
export const MultipleItems: Story = {
|
|
102
|
+
render: (args: { onSelect: (id: string) => void }) => (
|
|
103
|
+
<div class="w-64 space-y-0.5">
|
|
104
|
+
<ConversationItem
|
|
105
|
+
conversation={{ ...baseConversation, id: '1', title: 'SolidJS reactive primitives' }}
|
|
106
|
+
isActive={true}
|
|
107
|
+
onSelect={args.onSelect}
|
|
108
|
+
/>
|
|
109
|
+
<ConversationItem
|
|
110
|
+
conversation={{ ...baseConversation, id: '2', title: 'TypeScript generics guide', messageCount: 12 }}
|
|
111
|
+
isActive={false}
|
|
112
|
+
onSelect={args.onSelect}
|
|
113
|
+
/>
|
|
114
|
+
<ConversationItem
|
|
115
|
+
conversation={{ ...baseConversation, id: '3', title: 'Tailwind CSS tips and tricks', messageCount: 3 }}
|
|
116
|
+
isActive={false}
|
|
117
|
+
onSelect={args.onSelect}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
),
|
|
121
|
+
...src(`<div class="space-y-0.5">
|
|
122
|
+
<ConversationItem conversation={c1} isActive onSelect={onSelect} />
|
|
123
|
+
<ConversationItem conversation={c2} isActive={false} onSelect={onSelect} />
|
|
124
|
+
<ConversationItem conversation={c3} isActive={false} onSelect={onSelect} />
|
|
125
|
+
</div>`),
|
|
126
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { splitProps } from 'solid-js';
|
|
2
|
+
import { cn } from '../utils/cn';
|
|
3
|
+
import { useChatConfig, textClass } from '../primitives/chat-config';
|
|
4
|
+
import type { ConversationSummary } from '../types';
|
|
5
|
+
|
|
6
|
+
export interface ConversationItemProps { conversation: ConversationSummary; isActive: boolean; onSelect: (id: string) => void; class?: string; }
|
|
7
|
+
|
|
8
|
+
export function ConversationItem(props: ConversationItemProps) {
|
|
9
|
+
const [local] = splitProps(props, ['conversation', 'isActive', 'onSelect', 'class']);
|
|
10
|
+
const config = useChatConfig();
|
|
11
|
+
return (
|
|
12
|
+
<button data-conversation-id={local.conversation.id} onClick={() => local.onSelect(local.conversation.id)}
|
|
13
|
+
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)}>
|
|
14
|
+
<div class={cn('truncate', textClass(config.proseSize()), local.isActive ? 'text-foreground font-medium' : 'text-muted-foreground')}>{local.conversation.title}</div>
|
|
15
|
+
<div class={cn('text-muted-foreground/60 truncate mt-0.5', config.proseSize() === 'lg' ? 'text-sm' : config.proseSize() === 'base' ? 'text-xs' : 'text-[11px]')}>{local.conversation.messageCount} messages</div>
|
|
16
|
+
</button>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-solidjs-vite';
|
|
2
|
+
import { fn } from 'storybook/test';
|
|
3
|
+
import { ConversationList, type ConversationListProps } from './conversation-list';
|
|
4
|
+
import type { ConversationSummary, ConversationGroup } from '../types';
|
|
5
|
+
|
|
6
|
+
const scope = { type: 'document' as const };
|
|
7
|
+
|
|
8
|
+
const groups: ConversationGroup[] = [
|
|
9
|
+
{ id: 'today', name: 'Today', sortOrder: 0, createdAt: '2026-04-10' },
|
|
10
|
+
{ id: 'yesterday', name: 'Yesterday', sortOrder: 1, createdAt: '2026-04-09' },
|
|
11
|
+
{ id: 'week', name: 'This Week', sortOrder: 2, createdAt: '2026-04-07' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const conversations: ConversationSummary[] = [
|
|
15
|
+
{ id: '1', title: 'SolidJS signals explained', groupId: 'today', scope, messageCount: 5, lastMessageAt: '2026-04-10T14:00:00Z', updatedAt: '2026-04-10T14:00:00Z' },
|
|
16
|
+
{ id: '2', title: 'TypeScript generics deep dive', groupId: 'today', scope, messageCount: 12, lastMessageAt: '2026-04-10T10:00:00Z', updatedAt: '2026-04-10T10:00:00Z' },
|
|
17
|
+
{ id: '3', title: 'CSS Grid vs Flexbox', groupId: 'yesterday', scope, messageCount: 8, lastMessageAt: '2026-04-09T16:00:00Z', updatedAt: '2026-04-09T16:00:00Z' },
|
|
18
|
+
{ id: '4', title: 'Setting up Storybook', groupId: 'yesterday', scope, messageCount: 3, lastMessageAt: '2026-04-09T11:00:00Z', updatedAt: '2026-04-09T11:00:00Z' },
|
|
19
|
+
{ id: '5', title: 'Vite configuration tips', groupId: 'week', scope, messageCount: 7, lastMessageAt: '2026-04-08T09:00:00Z', updatedAt: '2026-04-08T09:00:00Z' },
|
|
20
|
+
{ id: '6', title: 'Chrome extension manifest v3', groupId: 'week', scope, messageCount: 15, lastMessageAt: '2026-04-07T14:00:00Z', updatedAt: '2026-04-07T14:00:00Z' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The conversation/chat history sidebar: a header with new-chat action, a search
|
|
25
|
+
* box, and grouped, collapsible lists of `ConversationItem`s.
|
|
26
|
+
*/
|
|
27
|
+
const meta = {
|
|
28
|
+
title: 'Components/ConversationList',
|
|
29
|
+
component: ConversationList,
|
|
30
|
+
tags: ['autodocs'],
|
|
31
|
+
parameters: {
|
|
32
|
+
layout: 'padded',
|
|
33
|
+
docs: {
|
|
34
|
+
description: {
|
|
35
|
+
component: [
|
|
36
|
+
'A full chat-history sidebar: header (sidebar toggle + new chat), a built-in search box that filters by title, and conversations bucketed into collapsible, count-badged groups.',
|
|
37
|
+
'**When to use:** as the left-hand navigation for a chat app — browsing, searching, and switching between past conversations.',
|
|
38
|
+
'**How to use:** pass `groups` and `conversations` arrays, the `activeId`, and handlers `onSelect(id)` / `onNewChat()` (plus optional `onToggleSidebar()`). Give it a sized, overflow-hidden container.',
|
|
39
|
+
'**Placement:** the persistent left sidebar of a chat layout.',
|
|
40
|
+
].join('\n\n'),
|
|
41
|
+
},
|
|
42
|
+
controls: { exclude: ['use:eventListener'] },
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
argTypes: {
|
|
46
|
+
groups: {
|
|
47
|
+
control: 'object',
|
|
48
|
+
description: 'Conversation groups (buckets) to render as collapsible sections.',
|
|
49
|
+
},
|
|
50
|
+
conversations: {
|
|
51
|
+
control: 'object',
|
|
52
|
+
description: 'Conversation summaries; placed into groups by their `groupId`.',
|
|
53
|
+
},
|
|
54
|
+
activeId: {
|
|
55
|
+
control: 'text',
|
|
56
|
+
description: 'Id of the currently selected conversation.',
|
|
57
|
+
},
|
|
58
|
+
onSelect: {
|
|
59
|
+
action: 'select',
|
|
60
|
+
description: 'Fired with the conversation id when a row is clicked.',
|
|
61
|
+
table: { category: 'Events' },
|
|
62
|
+
},
|
|
63
|
+
onNewChat: {
|
|
64
|
+
action: 'newChat',
|
|
65
|
+
description: 'Fired when the new-chat (+) button is clicked.',
|
|
66
|
+
table: { category: 'Events' },
|
|
67
|
+
},
|
|
68
|
+
onToggleSidebar: {
|
|
69
|
+
action: 'toggleSidebar',
|
|
70
|
+
description: 'Fired when the sidebar-toggle (menu) button is clicked.',
|
|
71
|
+
table: { category: 'Events' },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
args: {
|
|
75
|
+
groups,
|
|
76
|
+
conversations,
|
|
77
|
+
activeId: '1',
|
|
78
|
+
onSelect: fn(),
|
|
79
|
+
onNewChat: fn(),
|
|
80
|
+
onToggleSidebar: fn(),
|
|
81
|
+
},
|
|
82
|
+
render: (args) => (
|
|
83
|
+
<div class="h-[500px] w-72 border border-border rounded-lg overflow-hidden">
|
|
84
|
+
<ConversationList {...args} />
|
|
85
|
+
</div>
|
|
86
|
+
),
|
|
87
|
+
} satisfies Meta<typeof ConversationList>;
|
|
88
|
+
|
|
89
|
+
export default meta;
|
|
90
|
+
type Story = StoryObj<typeof meta>;
|
|
91
|
+
|
|
92
|
+
const IMPORT = `import { ConversationList } from '@kitnai/chat';`;
|
|
93
|
+
const src = (code: string) => ({
|
|
94
|
+
parameters: { docs: { source: { code: `${IMPORT}\n\n${code}`, language: 'tsx' } } },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const usage = `<ConversationList
|
|
98
|
+
groups={groups}
|
|
99
|
+
conversations={conversations}
|
|
100
|
+
activeId={activeId()}
|
|
101
|
+
onSelect={setActiveId}
|
|
102
|
+
onNewChat={() => {}}
|
|
103
|
+
/>`;
|
|
104
|
+
|
|
105
|
+
/** Interactive playground — edit the groups/conversations arrays and active id via controls. */
|
|
106
|
+
export const Playground: Story = {
|
|
107
|
+
...src(usage),
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Conversations bucketed into Today / Yesterday / This Week. */
|
|
111
|
+
export const WithGroups: Story = {
|
|
112
|
+
...src(usage),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/** No conversations — the list renders just its header and search box. */
|
|
116
|
+
export const EmptyState: Story = {
|
|
117
|
+
args: { groups: [], conversations: [], activeId: undefined },
|
|
118
|
+
render: (args: ConversationListProps) => (
|
|
119
|
+
<div class="h-[400px] w-72 border border-border rounded-lg overflow-hidden">
|
|
120
|
+
<ConversationList {...args} />
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
...src(`<ConversationList
|
|
124
|
+
groups={[]}
|
|
125
|
+
conversations={[]}
|
|
126
|
+
onSelect={() => {}}
|
|
127
|
+
onNewChat={() => {}}
|
|
128
|
+
/>`),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Type in the built-in search box to filter conversations by title. */
|
|
132
|
+
export const WithSearch: Story = {
|
|
133
|
+
...src(usage),
|
|
134
|
+
};
|