@kitnai/chat 0.4.0 → 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.
Files changed (35) hide show
  1. package/README.md +24 -5
  2. package/dist/custom-elements.json +475 -0
  3. package/dist/kitn-chat.es.js +18 -18
  4. package/dist/llms/llms-full.txt +55 -4
  5. package/dist/llms/llms.txt +3 -3
  6. package/dist/theme.tokens.css +6 -2
  7. package/frameworks/react/index.tsx +54 -0
  8. package/llms-full.txt +55 -4
  9. package/llms.txt +3 -3
  10. package/package.json +20 -2
  11. package/src/components/chat-thread.tsx +217 -0
  12. package/src/components/prompt-input.tsx +5 -0
  13. package/src/elements/chat-workspace.tsx +122 -0
  14. package/src/elements/chat.tsx +30 -271
  15. package/src/elements/compiled.css +1 -1
  16. package/src/elements/define.tsx +1 -1
  17. package/src/elements/element-types.d.ts +40 -0
  18. package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
  19. package/src/elements/register.ts +1 -0
  20. package/src/elements/styles.css +14 -0
  21. package/src/primitives/chat-config.tsx +1 -1
  22. package/src/stories/docs/Installation.mdx +27 -0
  23. package/src/stories/docs/Integrations.mdx +2 -0
  24. package/src/stories/docs/Introduction.mdx +12 -3
  25. package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
  26. package/src/stories/pattern-docked-widget.stories.tsx +93 -0
  27. package/src/stories/pattern-empty-state.stories.tsx +76 -0
  28. package/src/ui/collapsible.stories.tsx +70 -0
  29. package/src/ui/dropdown.stories.tsx +60 -0
  30. package/src/ui/hover-card.stories.tsx +78 -0
  31. package/src/ui/overlay.stories.tsx +115 -0
  32. package/src/ui/overlay.tsx +1 -1
  33. package/src/ui/scroll-area.stories.tsx +51 -0
  34. package/src/ui/textarea.stories.tsx +77 -0
  35. package/theme.css +6 -2
@@ -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
+ });
@@ -1,278 +1,37 @@
1
- import { createSignal, For, Show } from 'solid-js';
2
1
  import { defineKitnElement } from './define';
3
- import { ChatConfig, useChatConfig } from '../primitives/chat-config';
4
- import { ChatContainer, ChatContainerContent, ChatContainerScrollAnchor } from '../components/chat-container';
5
- import { Message, MessageContent, MessageActions } from '../components/message';
6
- import { Reasoning, ReasoningTrigger, ReasoningContent } from '../components/reasoning';
7
- import { Tool } from '../components/tool';
8
- import { Attachments, Attachment, AttachmentPreview, AttachmentInfo, type AttachmentData } from '../components/attachments';
9
- import { ModelSwitcher } from '../components/model-switcher';
10
- import { ScrollButton } from '../components/scroll-button';
11
- import {
12
- Context,
13
- ContextTrigger,
14
- ContextContent,
15
- ContextContentHeader,
16
- ContextContentBody,
17
- ContextContentFooter,
18
- ContextInputUsage,
19
- ContextOutputUsage,
20
- } from '../components/context';
21
- import { Button } from '../ui/button';
22
- import { Copy, ThumbsUp, ThumbsDown, RefreshCw, Pencil } from 'lucide-solid';
23
- import type { Component } from 'solid-js';
24
- import { DefaultPromptInput } from './default-input';
2
+ import { ChatThread, type ChatThreadProps, type ChatThreadContextUsage } from '../components/chat-thread';
25
3
  import type { SlashCommandItem } from '../components/slash-command';
26
- import type { ChatMessage, ChatMessageAction } from './chat-types';
27
4
  import type { ProseSize } from '../primitives/chat-config';
28
5
  import type { ModelOption } from '../types';
29
6
 
30
- interface ContextUsage {
31
- usedTokens: number;
32
- maxTokens: number;
33
- inputTokens?: number;
34
- outputTokens?: number;
35
- estimatedCost?: number;
36
- }
37
-
38
- interface Props extends Record<string, unknown> {
39
- /** The full message thread to render, newest last. Each entry carries its role,
40
- * content, and optional reasoning/tools/attachments/actions. Set as a JS
41
- * property (`el.messages = [...]`). */
42
- messages: ChatMessage[];
43
- /** Controlled value of the input. When set, the host owns the input text and
44
- * must update it on `valuechange`; leave unset for uncontrolled behavior. */
45
- value?: string;
46
- /** Placeholder text shown in the empty input. */
47
- placeholder?: string;
48
- /** When true, shows the loading/streaming state and disables submit (use while
49
- * awaiting the assistant's reply). */
50
- loading?: boolean;
51
- /** Starter prompts shown above the input when the thread is empty. Clicking one
52
- * follows `suggestionMode`. Set as a JS property. */
53
- suggestions?: string[];
54
- /** What clicking a suggestion does: `'submit'` (default) sends it immediately
55
- * as if typed and submitted; `'fill'` just places it in the input. */
56
- suggestionMode?: 'submit' | 'fill';
57
- /** Body/prose font scale for rendered markdown (`'xs' | 'sm' | 'base' | 'lg'`).
58
- * Defaults to `'sm'`. */
59
- proseSize?: ProseSize;
60
- /** Shiki theme name for syntax-highlighted code blocks (e.g.
61
- * `'github-dark-dimmed'`). */
62
- codeTheme?: string;
63
- /** Enable Shiki syntax highlighting in code blocks. Turn off to render plain
64
- * `<pre>` blocks (lighter, no highlighter load). Default true. */
65
- codeHighlight?: boolean;
66
- /** Optional header title shown on the left of the header. */
67
- chatTitle?: string;
68
- /** Optional model list. When set (>1 model) a ModelSwitcher is shown in the
69
- * header and a `modelchange` event fires on selection. */
70
- models?: ModelOption[];
71
- /** The currently selected model id (pairs with `models`). */
72
- currentModel?: string;
73
- /** Optional context-window token usage. When set, a Context token meter is
74
- * shown in the header. */
75
- context?: ContextUsage;
76
- /** Show the scroll-to-bottom button inside the scroll area. Default true. */
77
- scrollButton?: boolean;
78
- /** Show a Search (Globe) button in the input toolbar; fires a `search` event. */
79
- search?: boolean;
80
- /** Show a Voice (Mic) button in the input toolbar; fires a `voice` event. */
81
- voice?: boolean;
82
- /** Slash commands — when set, typing `/` in the input opens the command
83
- * palette and fires `slashselect`. Set as a JS property. */
84
- slashCommands?: SlashCommandItem[];
85
- /** Command ids to highlight as active in the palette. */
86
- slashActiveIds?: string[];
87
- /** Single-line palette rows. */
88
- slashCompact?: boolean;
89
- }
90
-
91
- const ACTION_LABEL: Record<ChatMessageAction, string> = {
92
- copy: 'Copy', like: 'Like', dislike: 'Dislike', regenerate: 'Regenerate', edit: 'Edit',
93
- };
94
-
95
- const ACTION_ICON: Record<ChatMessageAction, Component<{ class?: string }>> = {
96
- copy: Copy, like: ThumbsUp, dislike: ThumbsDown, regenerate: RefreshCw, edit: Pencil,
97
- };
7
+ type Props = Omit<ChatThreadProps,
8
+ 'class' | 'onValueChange' | 'onSubmit' | 'onSuggestionClick' | 'onModelChange'
9
+ | 'onMessageAction' | 'onSearch' | 'onVoice' | 'onSlashSelect'> & Record<string, unknown>;
98
10
 
99
11
  defineKitnElement<Props>('kitn-chat', {
100
- messages: [],
101
- value: undefined,
102
- placeholder: 'Send a message...',
103
- loading: false,
104
- suggestions: undefined,
105
- suggestionMode: 'submit',
106
- proseSize: 'sm',
107
- codeTheme: 'github-dark-dimmed',
108
- codeHighlight: true,
109
- chatTitle: undefined,
110
- models: undefined,
111
- currentModel: undefined,
112
- context: undefined,
113
- scrollButton: true,
114
- search: false,
115
- voice: false,
116
- slashCommands: undefined,
117
- slashActiveIds: undefined,
118
- slashCompact: false,
119
- }, (props, { dispatch, flag }) => {
120
- // Preserve the shadow-root portal mount from the wrapper's outer ChatConfig
121
- // when we nest a second ChatConfig to set proseSize/codeTheme.
122
- const outer = useChatConfig();
123
-
124
- const [internal, setInternal] = createSignal(props.value ?? '');
125
- const [attachments, setAttachments] = createSignal<AttachmentData[]>([]);
126
- const current = () => props.value ?? internal();
127
- const handleChange = (v: string) => { setInternal(v); dispatch('valuechange', { value: v }); };
128
- const handleSubmit = () => {
129
- dispatch('submit', { value: current(), attachments: attachments() });
130
- setAttachments([]);
131
- };
132
- const handleSuggestionClick = (v: string) => {
133
- if ((props.suggestionMode ?? 'submit') === 'fill') {
134
- handleChange(v);
135
- dispatch('suggestionclick', { value: v });
136
- } else {
137
- // Default: behave as if the user typed the suggestion and pressed submit.
138
- dispatch('submit', { value: v, attachments: attachments() });
139
- setAttachments([]);
140
- }
141
- };
142
-
143
- const showHeader = () => !!(props.chatTitle || props.models || props.context);
144
- const showScrollButton = () => props.scrollButton !== false;
145
-
146
- return (
147
- <ChatConfig proseSize={props.proseSize} codeTheme={props.codeTheme} codeHighlight={flag('codeHighlight')} portalMount={outer.portalMount()}>
148
- <div class="flex h-full flex-col bg-background">
149
- <Show when={showHeader()}>
150
- <header class="flex h-14 shrink-0 items-center justify-between border-b border-border px-5">
151
- <div class="text-sm font-semibold text-foreground">
152
- {props.chatTitle}
153
- </div>
154
- <div class="flex items-center gap-2">
155
- <Show when={props.models}>
156
- <ModelSwitcher
157
- models={props.models!}
158
- currentModelId={props.currentModel ?? props.models![0]?.id ?? ''}
159
- onModelChange={(modelId) => dispatch('modelchange', { modelId })}
160
- />
161
- </Show>
162
- <Show when={props.context}>
163
- <Context
164
- usedTokens={props.context!.usedTokens}
165
- maxTokens={props.context!.maxTokens}
166
- inputTokens={props.context!.inputTokens}
167
- outputTokens={props.context!.outputTokens}
168
- estimatedCost={props.context!.estimatedCost}
169
- >
170
- <ContextTrigger />
171
- <ContextContent>
172
- <ContextContentHeader />
173
- <ContextContentBody>
174
- <div class="space-y-1.5">
175
- <ContextInputUsage />
176
- <ContextOutputUsage />
177
- </div>
178
- </ContextContentBody>
179
- <ContextContentFooter />
180
- </ContextContent>
181
- </Context>
182
- </Show>
183
- </div>
184
- </header>
185
- </Show>
186
- <div class="relative flex-1 overflow-hidden">
187
- <ChatContainer class="h-full px-4 py-3">
188
- <ChatContainerContent class="mx-auto w-full max-w-3xl space-y-4">
189
- <For each={props.messages}>
190
- {(m) => (
191
- <Message class={m.role === 'user' ? 'flex-col items-end' : 'flex-col items-start'}>
192
- <Show when={m.reasoning}>
193
- <Reasoning class="mb-2 w-full">
194
- <ReasoningTrigger>{m.reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
195
- <ReasoningContent markdown>{m.reasoning!.text}</ReasoningContent>
196
- </Reasoning>
197
- </Show>
198
- <For each={m.tools ?? []}>
199
- {(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}
200
- </For>
201
- <Show when={m.attachments?.length}>
202
- <Attachments variant="inline" class={m.role === 'user' ? 'mb-2 justify-end' : 'mb-2'}>
203
- <For each={m.attachments!}>
204
- {(att) => (
205
- <Attachment data={att}>
206
- <AttachmentPreview />
207
- <AttachmentInfo />
208
- </Attachment>
209
- )}
210
- </For>
211
- </Attachments>
212
- </Show>
213
- <MessageContent
214
- markdown={m.role === 'assistant'}
215
- class={m.role === 'user'
216
- ? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2'
217
- : 'bg-transparent p-0'}
218
- >
219
- {m.content}
220
- </MessageContent>
221
- <Show when={m.actions?.length}>
222
- <MessageActions class="mt-1 flex gap-0">
223
- <For each={m.actions!}>
224
- {(a) => (
225
- <Button
226
- variant="ghost" size="icon-sm" class="rounded-full"
227
- data-action={a}
228
- aria-label={ACTION_LABEL[a]}
229
- onClick={() => dispatch('messageaction', { messageId: m.id, action: a })}
230
- >
231
- {(() => {
232
- const Icon = ACTION_ICON[a];
233
- return <Icon class="size-3.5" />;
234
- })()}
235
- </Button>
236
- )}
237
- </For>
238
- </MessageActions>
239
- </Show>
240
- </Message>
241
- )}
242
- </For>
243
- <ChatContainerScrollAnchor />
244
- </ChatContainerContent>
245
- <Show when={showScrollButton()}>
246
- <div class="absolute bottom-4 left-1/2 flex w-full max-w-3xl -translate-x-1/2 justify-center px-5">
247
- <ScrollButton class="shadow-sm" />
248
- </div>
249
- </Show>
250
- </ChatContainer>
251
- </div>
252
- <div class="shrink-0 px-4 pb-4">
253
- <div class="mx-auto max-w-3xl">
254
- <DefaultPromptInput
255
- value={current()}
256
- placeholder={props.placeholder}
257
- loading={flag('loading')}
258
- suggestions={props.suggestions}
259
- attachments={attachments()}
260
- search={flag('search')}
261
- voice={flag('voice')}
262
- slashCommands={props.slashCommands}
263
- slashActiveIds={props.slashActiveIds}
264
- slashCompact={flag('slashCompact')}
265
- onValueChange={handleChange}
266
- onSubmit={handleSubmit}
267
- onSuggestionClick={handleSuggestionClick}
268
- onAttachmentsChange={setAttachments}
269
- onSearch={() => dispatch('search', {})}
270
- onVoice={() => dispatch('voice', {})}
271
- onSlashSelect={(command) => dispatch('slashselect', { command })}
272
- />
273
- </div>
274
- </div>
275
- </div>
276
- </ChatConfig>
277
- );
278
- });
12
+ messages: [], value: undefined, placeholder: 'Send a message...', loading: false,
13
+ suggestions: undefined, suggestionMode: 'submit', proseSize: 'sm',
14
+ codeTheme: 'github-dark-dimmed', codeHighlight: true, chatTitle: undefined,
15
+ models: undefined, currentModel: undefined, context: undefined, scrollButton: true,
16
+ search: false, voice: false, slashCommands: undefined, slashActiveIds: undefined, slashCompact: false,
17
+ }, (props, { dispatch, flag }) => (
18
+ <ChatThread
19
+ messages={props.messages} value={props.value as string | undefined} placeholder={props.placeholder as string}
20
+ loading={flag('loading')} suggestions={props.suggestions as string[] | undefined}
21
+ suggestionMode={props.suggestionMode as 'submit' | 'fill'} proseSize={props.proseSize as ProseSize}
22
+ codeTheme={props.codeTheme as string} codeHighlight={flag('codeHighlight')}
23
+ chatTitle={props.chatTitle as string | undefined} models={props.models as ModelOption[] | undefined}
24
+ currentModel={props.currentModel as string | undefined} context={props.context as ChatThreadContextUsage | undefined}
25
+ scrollButton={props.scrollButton !== false} search={flag('search')} voice={flag('voice')}
26
+ slashCommands={props.slashCommands as SlashCommandItem[] | undefined}
27
+ slashActiveIds={props.slashActiveIds as string[] | undefined} slashCompact={flag('slashCompact')}
28
+ onValueChange={(value) => dispatch('valuechange', { value })}
29
+ onSubmit={(detail) => dispatch('submit', detail)}
30
+ onSuggestionClick={(value) => dispatch('suggestionclick', { value })}
31
+ onModelChange={(modelId) => dispatch('modelchange', { modelId })}
32
+ onMessageAction={(detail) => dispatch('messageaction', detail)}
33
+ onSearch={() => dispatch('search', {})}
34
+ onVoice={() => dispatch('voice', {})}
35
+ onSlashSelect={(command) => dispatch('slashselect', { command })}
36
+ />
37
+ ));