@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.
Files changed (119) hide show
  1. package/README.md +35 -5
  2. package/dist/custom-elements.json +2969 -0
  3. package/dist/kitn-chat.es.js +52 -39
  4. package/dist/llms/llms-full.txt +718 -0
  5. package/dist/llms/llms.txt +104 -0
  6. package/dist/theme.tokens.css +137 -0
  7. package/frameworks/react/index.tsx +584 -0
  8. package/frameworks/react/runtime.tsx +94 -0
  9. package/llms-full.txt +718 -0
  10. package/llms.txt +104 -0
  11. package/package.json +53 -6
  12. package/src/components/attachments.tsx +4 -2
  13. package/src/components/chain-of-thought.tsx +1 -1
  14. package/src/components/chat-scope-picker.tsx +2 -2
  15. package/src/components/chat-thread.tsx +217 -0
  16. package/src/components/checkpoint.tsx +7 -3
  17. package/src/components/context.tsx +14 -18
  18. package/src/components/conversation-item.tsx +1 -1
  19. package/src/components/conversation-list.tsx +5 -4
  20. package/src/components/message-skills.tsx +1 -1
  21. package/src/components/message.tsx +1 -0
  22. package/src/components/model-switcher.tsx +3 -3
  23. package/src/components/prompt-input.tsx +20 -2
  24. package/src/components/reasoning.tsx +2 -2
  25. package/src/components/scroll-button.tsx +1 -0
  26. package/src/components/slash-command.tsx +17 -8
  27. package/src/components/source.tsx +2 -2
  28. package/src/components/thinking-bar.tsx +2 -2
  29. package/src/components/tool.tsx +17 -6
  30. package/src/components/voice-input.tsx +5 -1
  31. package/src/elements/attachments.tsx +132 -0
  32. package/src/elements/chain-of-thought.tsx +45 -0
  33. package/src/elements/chat-scope-picker.tsx +36 -0
  34. package/src/elements/chat-workspace.tsx +122 -0
  35. package/src/elements/chat.tsx +31 -228
  36. package/src/elements/checkpoint.tsx +43 -0
  37. package/src/elements/code-block.tsx +42 -0
  38. package/src/elements/compiled.css +1 -1
  39. package/src/elements/context-meter.tsx +71 -0
  40. package/src/elements/conversation-list.tsx +6 -0
  41. package/src/elements/default-input.tsx +22 -1
  42. package/src/elements/define.tsx +98 -12
  43. package/src/elements/element-types.d.ts +444 -0
  44. package/src/elements/empty.tsx +29 -0
  45. package/src/elements/feedback-bar.tsx +33 -0
  46. package/src/elements/file-upload.tsx +44 -0
  47. package/src/elements/image.tsx +32 -0
  48. package/src/elements/kitn-attachments.stories.tsx +181 -0
  49. package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
  50. package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
  51. package/src/elements/kitn-chat-workspace.stories.tsx +195 -0
  52. package/src/elements/kitn-checkpoint.stories.tsx +71 -0
  53. package/src/elements/kitn-code-block.stories.tsx +82 -0
  54. package/src/elements/kitn-context-meter.stories.tsx +85 -0
  55. package/src/elements/kitn-empty.stories.tsx +110 -0
  56. package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
  57. package/src/elements/kitn-file-upload.stories.tsx +81 -0
  58. package/src/elements/kitn-image.stories.tsx +70 -0
  59. package/src/elements/kitn-loader.stories.tsx +87 -0
  60. package/src/elements/kitn-markdown.stories.tsx +75 -0
  61. package/src/elements/kitn-message-skills.stories.tsx +74 -0
  62. package/src/elements/kitn-message.stories.tsx +105 -0
  63. package/src/elements/kitn-model-switcher.stories.tsx +80 -0
  64. package/src/elements/kitn-prompt-input.stories.tsx +74 -16
  65. package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
  66. package/src/elements/kitn-reasoning.stories.tsx +76 -0
  67. package/src/elements/kitn-response-stream.stories.tsx +79 -0
  68. package/src/elements/kitn-source-list.stories.tsx +77 -0
  69. package/src/elements/kitn-source.stories.tsx +87 -0
  70. package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
  71. package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
  72. package/src/elements/kitn-tool.stories.tsx +88 -0
  73. package/src/elements/kitn-voice-input.stories.tsx +87 -0
  74. package/src/elements/loader.tsx +25 -0
  75. package/src/elements/markdown.tsx +38 -0
  76. package/src/elements/message-skills.tsx +22 -0
  77. package/src/elements/message.tsx +125 -0
  78. package/src/elements/model-switcher.tsx +35 -0
  79. package/src/elements/prompt-input.tsx +83 -7
  80. package/src/elements/prompt-suggestions.tsx +58 -0
  81. package/src/elements/reasoning.tsx +50 -0
  82. package/src/elements/register.ts +32 -0
  83. package/src/elements/response-stream.tsx +40 -0
  84. package/src/elements/source.tsx +67 -0
  85. package/src/elements/styles.css +14 -0
  86. package/src/elements/text-shimmer.tsx +28 -0
  87. package/src/elements/thinking-bar.tsx +34 -0
  88. package/src/elements/tool.tsx +23 -0
  89. package/src/elements/voice-input.tsx +41 -0
  90. package/src/index.ts +0 -1
  91. package/src/primitives/chat-config.tsx +3 -3
  92. package/src/stories/docs/Accessibility.mdx +119 -0
  93. package/src/stories/docs/ForAIAgents.mdx +93 -0
  94. package/src/stories/docs/GettingStarted.mdx +2 -2
  95. package/src/stories/docs/Installation.mdx +29 -2
  96. package/src/stories/docs/Integrations.mdx +417 -15
  97. package/src/stories/docs/Introduction.mdx +17 -8
  98. package/src/stories/docs/Theming.mdx +1 -1
  99. package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
  100. package/src/stories/pattern-docked-widget.stories.tsx +93 -0
  101. package/src/stories/pattern-empty-state.stories.tsx +76 -0
  102. package/src/stories/typography.stories.tsx +78 -0
  103. package/src/ui/button.tsx +1 -1
  104. package/src/ui/collapsible.stories.tsx +70 -0
  105. package/src/ui/collapsible.tsx +119 -8
  106. package/src/ui/dropdown.stories.tsx +60 -0
  107. package/src/ui/dropdown.tsx +177 -12
  108. package/src/ui/hover-card.stories.tsx +78 -0
  109. package/src/ui/hover-card.tsx +147 -26
  110. package/src/ui/overlay.stories.tsx +115 -0
  111. package/src/ui/overlay.tsx +151 -0
  112. package/src/ui/scroll-area.stories.tsx +51 -0
  113. package/src/ui/textarea.stories.tsx +77 -0
  114. package/src/ui/textarea.tsx +1 -1
  115. package/src/ui/tooltip.stories.tsx +1 -1
  116. package/src/ui/tooltip.tsx +59 -13
  117. package/src/utils/cn.ts +19 -1
  118. package/theme.css +76 -43
  119. 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
- adjustHeight(e.currentTarget);
125
- ctx.setValue(e.currentTarget.value);
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
  >
@@ -16,6 +16,7 @@ function ScrollButton(props: ScrollButtonProps) {
16
16
  <Button
17
17
  variant={props.variant ?? 'outline'}
18
18
  size={props.size ?? 'sm'}
19
+ aria-label="Scroll to bottom"
19
20
  class={cn(
20
21
  'h-10 w-10 rounded-full transition-all duration-150 ease-out',
21
22
  !isAtBottom()
@@ -99,11 +99,20 @@ function SlashCommand(props: SlashCommandProps) {
99
99
  });
100
100
 
101
101
  function selectItem(item: SlashCommandItem) {
102
- ctx.setValue("");
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(() => ctx.textareaRef?.focus(), 0);
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/60 uppercase tracking-wide">
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/50 truncate">
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/40 truncate">{item.description}</span>
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} closeDelay={0}>
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>
@@ -46,10 +46,19 @@ function ToolStateIcon(props: { state: ToolPart['state'] }) {
46
46
  );
47
47
  }
48
48
 
49
- // Status chips: a saturated hue as text over a 15% translucent fill of the same
50
- // hue. This reads on both light and dark surfaces (mirroring the inline-code chip),
51
- // so it needs no `dark:` variant which wouldn't follow a token-scoped theme anyway.
52
- const STATE_HUE: Record<ToolPart['state'], string> = {
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
- const hue = STATE_HUE[state];
61
- return { color: hue, background: `color-mix(in oklab, ${hue} 15%, transparent)` };
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={isProcessing() ? 'Transcribing...' : isRecording() ? 'Stop recording' : 'Voice input'}>
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
+ });