@kitnai/chat 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +11 -0
  2. package/dist/custom-elements.json +2494 -0
  3. package/dist/kitn-chat.es.js +52 -39
  4. package/dist/llms/llms-full.txt +667 -0
  5. package/dist/llms/llms.txt +104 -0
  6. package/dist/theme.tokens.css +133 -0
  7. package/frameworks/react/index.tsx +530 -0
  8. package/frameworks/react/runtime.tsx +94 -0
  9. package/llms-full.txt +667 -0
  10. package/llms.txt +104 -0
  11. package/package.json +34 -5
  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/checkpoint.tsx +7 -3
  16. package/src/components/context.tsx +14 -18
  17. package/src/components/conversation-item.tsx +1 -1
  18. package/src/components/conversation-list.tsx +5 -4
  19. package/src/components/message-skills.tsx +1 -1
  20. package/src/components/message.tsx +1 -0
  21. package/src/components/model-switcher.tsx +3 -3
  22. package/src/components/prompt-input.tsx +15 -2
  23. package/src/components/reasoning.tsx +2 -2
  24. package/src/components/scroll-button.tsx +1 -0
  25. package/src/components/slash-command.tsx +17 -8
  26. package/src/components/source.tsx +2 -2
  27. package/src/components/thinking-bar.tsx +2 -2
  28. package/src/components/tool.tsx +17 -6
  29. package/src/components/voice-input.tsx +5 -1
  30. package/src/elements/attachments.tsx +132 -0
  31. package/src/elements/chain-of-thought.tsx +45 -0
  32. package/src/elements/chat-scope-picker.tsx +36 -0
  33. package/src/elements/chat.tsx +51 -7
  34. package/src/elements/checkpoint.tsx +43 -0
  35. package/src/elements/code-block.tsx +42 -0
  36. package/src/elements/compiled.css +1 -1
  37. package/src/elements/context-meter.tsx +71 -0
  38. package/src/elements/conversation-list.tsx +6 -0
  39. package/src/elements/default-input.tsx +22 -1
  40. package/src/elements/define.tsx +102 -13
  41. package/src/elements/element-types.d.ts +404 -0
  42. package/src/elements/empty.tsx +29 -0
  43. package/src/elements/feedback-bar.tsx +33 -0
  44. package/src/elements/file-upload.tsx +44 -0
  45. package/src/elements/image.tsx +32 -0
  46. package/src/elements/kitn-attachments.stories.tsx +181 -0
  47. package/src/elements/kitn-chain-of-thought.stories.tsx +75 -0
  48. package/src/elements/kitn-chat-scope-picker.stories.tsx +72 -0
  49. package/src/elements/kitn-checkpoint.stories.tsx +71 -0
  50. package/src/elements/kitn-code-block.stories.tsx +82 -0
  51. package/src/elements/kitn-context-meter.stories.tsx +85 -0
  52. package/src/elements/kitn-empty.stories.tsx +110 -0
  53. package/src/elements/kitn-feedback-bar.stories.tsx +73 -0
  54. package/src/elements/kitn-file-upload.stories.tsx +81 -0
  55. package/src/elements/kitn-image.stories.tsx +70 -0
  56. package/src/elements/kitn-loader.stories.tsx +87 -0
  57. package/src/elements/kitn-markdown.stories.tsx +75 -0
  58. package/src/elements/kitn-message-skills.stories.tsx +74 -0
  59. package/src/elements/kitn-message.stories.tsx +105 -0
  60. package/src/elements/kitn-model-switcher.stories.tsx +80 -0
  61. package/src/elements/kitn-prompt-input.stories.tsx +74 -16
  62. package/src/elements/kitn-prompt-suggestions.stories.tsx +157 -0
  63. package/src/elements/kitn-reasoning.stories.tsx +76 -0
  64. package/src/elements/kitn-response-stream.stories.tsx +79 -0
  65. package/src/elements/kitn-source-list.stories.tsx +77 -0
  66. package/src/elements/kitn-source.stories.tsx +87 -0
  67. package/src/elements/kitn-text-shimmer.stories.tsx +63 -0
  68. package/src/elements/kitn-thinking-bar.stories.tsx +72 -0
  69. package/src/elements/kitn-tool.stories.tsx +88 -0
  70. package/src/elements/kitn-voice-input.stories.tsx +87 -0
  71. package/src/elements/loader.tsx +25 -0
  72. package/src/elements/markdown.tsx +38 -0
  73. package/src/elements/message-skills.tsx +22 -0
  74. package/src/elements/message.tsx +125 -0
  75. package/src/elements/model-switcher.tsx +35 -0
  76. package/src/elements/prompt-input.tsx +83 -7
  77. package/src/elements/prompt-suggestions.tsx +58 -0
  78. package/src/elements/reasoning.tsx +50 -0
  79. package/src/elements/register.ts +31 -0
  80. package/src/elements/response-stream.tsx +40 -0
  81. package/src/elements/source.tsx +67 -0
  82. package/src/elements/text-shimmer.tsx +28 -0
  83. package/src/elements/thinking-bar.tsx +34 -0
  84. package/src/elements/tool.tsx +23 -0
  85. package/src/elements/voice-input.tsx +41 -0
  86. package/src/index.ts +0 -1
  87. package/src/primitives/chat-config.tsx +2 -2
  88. package/src/stories/docs/Accessibility.mdx +119 -0
  89. package/src/stories/docs/ForAIAgents.mdx +93 -0
  90. package/src/stories/docs/GettingStarted.mdx +2 -2
  91. package/src/stories/docs/Installation.mdx +2 -2
  92. package/src/stories/docs/Integrations.mdx +415 -15
  93. package/src/stories/docs/Introduction.mdx +5 -5
  94. package/src/stories/docs/Theming.mdx +1 -1
  95. package/src/stories/typography.stories.tsx +78 -0
  96. package/src/ui/button.tsx +1 -1
  97. package/src/ui/collapsible.tsx +119 -8
  98. package/src/ui/dropdown.tsx +177 -12
  99. package/src/ui/hover-card.tsx +147 -26
  100. package/src/ui/overlay.tsx +151 -0
  101. package/src/ui/textarea.tsx +1 -1
  102. package/src/ui/tooltip.stories.tsx +1 -1
  103. package/src/ui/tooltip.tsx +59 -13
  104. package/src/utils/cn.ts +19 -1
  105. package/theme.css +72 -43
  106. package/src/ui/dialog.tsx +0 -21
@@ -0,0 +1,63 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import './register'; // side effect: registers the custom elements
3
+
4
+ // The web components are custom DOM elements, so declare the tags for JSX.
5
+ declare module 'solid-js' {
6
+ // eslint-disable-next-line @typescript-eslint/no-namespace
7
+ namespace JSX {
8
+ interface IntrinsicElements {
9
+ 'kitn-text-shimmer': JSX.HTMLAttributes<HTMLElement> & {
10
+ text?: string;
11
+ duration?: number;
12
+ spread?: number;
13
+ };
14
+ }
15
+ }
16
+ }
17
+
18
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
19
+ <kitn-text-shimmer text="Thinking…" duration="3" spread="20"></kitn-text-shimmer>
20
+
21
+ <script type="module">
22
+ import '@kitnai/chat/elements'; // registers the custom elements
23
+ </script>`;
24
+
25
+ const meta = {
26
+ title: 'Web Components/kitn-text-shimmer',
27
+ tags: ['autodocs'],
28
+ parameters: {
29
+ layout: 'fullscreen',
30
+ docs: {
31
+ description: {
32
+ component: [
33
+ '`<kitn-text-shimmer>` is the framework-agnostic **web component** for animated shimmering text — a gradient sweep across a label, isolated in **Shadow DOM**.',
34
+ '**When to use:** signalling a quiet, in-progress state ("Thinking…", "Generating…") inline. In SolidJS, use the `TextShimmer` primitive.',
35
+ "**How to use:** register once with `import '@kitnai/chat/elements'`, set the `text` attribute, and tune `duration` (seconds) and `spread` (gradient width, 5–45).",
36
+ 'See the **Code** tab for HTML usage.',
37
+ ].join('\n\n'),
38
+ },
39
+ },
40
+ },
41
+ } satisfies Meta;
42
+
43
+ export default meta;
44
+ type Story = StoryObj;
45
+
46
+ /** Default shimmer. */
47
+ export const Default: Story = {
48
+ render: () => (
49
+ <div style={{ padding: '24px', 'font-size': '18px' }}>
50
+ <kitn-text-shimmer text="Thinking…" />
51
+ </div>
52
+ ),
53
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
54
+ };
55
+
56
+ /** Faster sweep with a wider gradient spread. */
57
+ export const Tuned: Story = {
58
+ render: () => (
59
+ <div style={{ padding: '24px', 'font-size': '18px' }}>
60
+ <kitn-text-shimmer text="Generating response…" duration={2} spread={35} />
61
+ </div>
62
+ ),
63
+ };
@@ -0,0 +1,72 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import './register'; // side effect: registers the custom elements
3
+
4
+ // The web components are custom DOM elements, so declare the tags for JSX.
5
+ declare module 'solid-js' {
6
+ // eslint-disable-next-line @typescript-eslint/no-namespace
7
+ namespace JSX {
8
+ interface IntrinsicElements {
9
+ 'kitn-thinking-bar': JSX.HTMLAttributes<HTMLElement> & {
10
+ text?: string;
11
+ stoppable?: boolean | string;
12
+ 'stop-label'?: string;
13
+ 'on:stop'?: (e: CustomEvent) => void;
14
+ };
15
+ }
16
+ }
17
+ }
18
+
19
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
20
+ <kitn-thinking-bar text="Thinking" stoppable stop-label="Answer now"></kitn-thinking-bar>
21
+
22
+ <script type="module">
23
+ import '@kitnai/chat/elements'; // registers the custom elements
24
+
25
+ document.querySelector('kitn-thinking-bar')
26
+ .addEventListener('stop', () => console.log('user asked to stop'));
27
+ </script>`;
28
+
29
+ const meta = {
30
+ title: 'Web Components/kitn-thinking-bar',
31
+ tags: ['autodocs'],
32
+ parameters: {
33
+ layout: 'fullscreen',
34
+ docs: {
35
+ description: {
36
+ component: [
37
+ '`<kitn-thinking-bar>` is the framework-agnostic **web component** for an animated "thinking" indicator with an optional stop affordance — a pure leaf element isolated in **Shadow DOM**. (`<kitn-chat>` does not surface this; compose it yourself.)',
38
+ '**When to use:** showing that the assistant is reasoning, optionally letting the user interrupt with "Answer now". In SolidJS, use the `ThinkingBar` primitive.',
39
+ "**How to use:** register once with `import '@kitnai/chat/elements'`, set the `text`/`stop-label` attributes, add the `stoppable` flag to show the stop button, and listen for the `stop` **CustomEvent**.",
40
+ 'See the **Code** tab for HTML usage.',
41
+ ].join('\n\n'),
42
+ },
43
+ },
44
+ },
45
+ } satisfies Meta;
46
+
47
+ export default meta;
48
+ type Story = StoryObj;
49
+
50
+ /** A plain thinking indicator. */
51
+ export const Default: Story = {
52
+ render: () => (
53
+ <div style={{ padding: '24px' }}>
54
+ <kitn-thinking-bar text="Thinking" />
55
+ </div>
56
+ ),
57
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
58
+ };
59
+
60
+ /** Stoppable — shows an "Answer now" affordance that fires a `stop` event. */
61
+ export const Stoppable: Story = {
62
+ render: () => (
63
+ <div style={{ padding: '24px' }}>
64
+ <kitn-thinking-bar
65
+ text="Reasoning"
66
+ stoppable
67
+ stop-label="Answer now"
68
+ on:stop={() => console.log('stop')}
69
+ />
70
+ </div>
71
+ ),
72
+ };
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { onMount } from 'solid-js';
3
+ import './register'; // side effect: registers the custom elements
4
+ import type { ToolPart } from '../components/tool';
5
+
6
+ // The web components are custom DOM elements, so declare the tags for JSX.
7
+ declare module 'solid-js' {
8
+ // eslint-disable-next-line @typescript-eslint/no-namespace
9
+ namespace JSX {
10
+ interface IntrinsicElements {
11
+ 'kitn-tool': JSX.HTMLAttributes<HTMLElement>;
12
+ }
13
+ }
14
+ }
15
+
16
+ const completedTool: ToolPart = {
17
+ type: 'database_query',
18
+ state: 'output-available',
19
+ input: { table: 'users', limit: 10 },
20
+ output: { rows: 10, ms: 42 },
21
+ };
22
+
23
+ const runningTool: ToolPart = {
24
+ type: 'search',
25
+ state: 'input-available',
26
+ input: { query: 'kitn docs' },
27
+ };
28
+
29
+ /** Render the actual `<kitn-tool>` custom element with a `tool` property. */
30
+ function ToolElement(props: { tool: ToolPart; open?: boolean }) {
31
+ let el: (HTMLElement & { tool?: ToolPart; open?: boolean }) | undefined;
32
+ onMount(() => {
33
+ if (el) {
34
+ el.tool = props.tool;
35
+ if (props.open) el.open = true;
36
+ }
37
+ });
38
+ return (
39
+ <kitn-tool ref={(e) => (el = e as HTMLElement)} style={{ display: 'block', padding: '16px', 'max-width': '720px' }} />
40
+ );
41
+ }
42
+
43
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
44
+ <kitn-tool id="tool" open></kitn-tool>
45
+
46
+ <script type="module">
47
+ import '@kitnai/chat/elements'; // registers the custom elements
48
+
49
+ const tool = document.getElementById('tool');
50
+ tool.tool = {
51
+ type: 'database_query',
52
+ state: 'output-available',
53
+ input: { table: 'users', limit: 10 },
54
+ output: { rows: 10, ms: 42 },
55
+ };
56
+ </script>`;
57
+
58
+ const meta = {
59
+ title: 'Web Components/kitn-tool',
60
+ tags: ['autodocs'],
61
+ parameters: {
62
+ layout: 'fullscreen',
63
+ docs: {
64
+ description: {
65
+ component: [
66
+ '`<kitn-tool>` is the framework-agnostic **web component** for a single tool-call panel — a collapsible input/output inspector with a state badge — isolated in **Shadow DOM**.',
67
+ '**When to use:** rendering an agent/tool-call trace in a non-Solid app. In SolidJS, use the `Tool` primitive directly.',
68
+ "**How to use:** register once with `import '@kitnai/chat/elements'`, set the call via the `tool` **property** (`el.tool = {...}`), and add the `open` flag to start it expanded.",
69
+ 'See the **Code** tab for HTML usage.',
70
+ ].join('\n\n'),
71
+ },
72
+ },
73
+ },
74
+ } satisfies Meta;
75
+
76
+ export default meta;
77
+ type Story = StoryObj;
78
+
79
+ /** A completed call with input and output, started expanded. */
80
+ export const Completed: Story = {
81
+ render: () => <ToolElement tool={completedTool} open />,
82
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
83
+ };
84
+
85
+ /** A call still awaiting output (collapsed). */
86
+ export const Running: Story = {
87
+ render: () => <ToolElement tool={runningTool} />,
88
+ };
@@ -0,0 +1,87 @@
1
+ import type { Meta, StoryObj } from 'storybook-solidjs-vite';
2
+ import { onMount } from 'solid-js';
3
+ import './register'; // side effect: registers the custom elements
4
+
5
+ // The web components are custom DOM elements, so declare the tags for JSX.
6
+ declare module 'solid-js' {
7
+ // eslint-disable-next-line @typescript-eslint/no-namespace
8
+ namespace JSX {
9
+ interface IntrinsicElements {
10
+ 'kitn-voice-input': JSX.HTMLAttributes<HTMLElement> & {
11
+ disabled?: boolean | string;
12
+ };
13
+ }
14
+ }
15
+ }
16
+
17
+ /** Render `<kitn-voice-input>` with a stub `transcribe` function-property. */
18
+ function VoiceElement(props: { disabled?: boolean }) {
19
+ let el: (HTMLElement & { transcribe?: (audio: Blob) => Promise<string> }) | undefined;
20
+ onMount(() => {
21
+ if (!el) return;
22
+ // transcribe MUST be set as a JS property — a value-returning callback
23
+ // can't be modelled as an attribute.
24
+ el.transcribe = async () => {
25
+ await new Promise((r) => setTimeout(r, 400));
26
+ return 'transcribed text';
27
+ };
28
+ el.addEventListener('transcription', (e) => {
29
+ const ev = e as CustomEvent<{ text: string }>;
30
+ console.log('transcription', ev.detail.text);
31
+ });
32
+ });
33
+ return (
34
+ <kitn-voice-input
35
+ ref={(e) => (el = e as HTMLElement)}
36
+ disabled={props.disabled ? true : undefined}
37
+ style={{ display: 'inline-block', padding: '40px' }}
38
+ />
39
+ );
40
+ }
41
+
42
+ const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
43
+ <kitn-voice-input id="voice"></kitn-voice-input>
44
+
45
+ <script type="module">
46
+ import '@kitnai/chat/elements'; // registers the custom elements
47
+
48
+ const voice = document.getElementById('voice');
49
+ // transcribe is a FUNCTION property — your async transcriber
50
+ voice.transcribe = async (blob) => {
51
+ const text = await myTranscriptionApi(blob);
52
+ return text;
53
+ };
54
+ voice.addEventListener('transcription', (e) => console.log(e.detail.text));
55
+ </script>`;
56
+
57
+ const meta = {
58
+ title: 'Web Components/kitn-voice-input',
59
+ tags: ['autodocs'],
60
+ parameters: {
61
+ layout: 'fullscreen',
62
+ docs: {
63
+ description: {
64
+ component: [
65
+ '`<kitn-voice-input>` is the framework-agnostic **web component** for a mic button that records and transcribes audio — isolated in **Shadow DOM**. It is the canonical **function-property** element.',
66
+ '**When to use:** adding voice dictation to an input in a non-Solid app. In SolidJS, use the `VoiceInput` primitive.',
67
+ "**How to use:** register once with `import '@kitnai/chat/elements'`, then set the `transcribe` **function property** (`el.transcribe = async blob => '...'`) — a value-returning callback can't be modelled as an event. It also emits `audiocaptured` (raw blob) and `transcription` (text) **CustomEvents**.",
68
+ 'See the **Code** tab for HTML usage.',
69
+ ].join('\n\n'),
70
+ },
71
+ },
72
+ },
73
+ } satisfies Meta;
74
+
75
+ export default meta;
76
+ type Story = StoryObj;
77
+
78
+ /** A working mic button wired to a stub transcriber. */
79
+ export const Default: Story = {
80
+ render: () => <VoiceElement />,
81
+ parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
82
+ };
83
+
84
+ /** A disabled mic button (non-interactive). */
85
+ export const Disabled: Story = {
86
+ render: () => <VoiceElement disabled />,
87
+ };
@@ -0,0 +1,25 @@
1
+ import { defineKitnElement } from './define';
2
+ import { Loader, type LoaderVariant, type LoaderSize } from '../components/loader';
3
+
4
+ interface Props extends Record<string, unknown> {
5
+ /** The animation style: `'circular' | 'classic' | 'pulse' | 'pulse-dot' |
6
+ * 'dots' | 'typing' | 'wave' | 'bars' | 'terminal' | 'text-blink' |
7
+ * 'text-shimmer' | 'loading-dots'`. Defaults to `'circular'`. */
8
+ variant?: LoaderVariant;
9
+ /** Loader size: `'sm' | 'md' | 'lg'`. Defaults to `'md'`. */
10
+ size?: LoaderSize;
11
+ /** Label for the text-based variants. */
12
+ text?: string;
13
+ }
14
+
15
+ /**
16
+ * `<kitn-loader>` — an animated loader. `variant` selects the style (circular,
17
+ * dots, wave, text-shimmer, …); `size` and `text` are attributes.
18
+ */
19
+ defineKitnElement<Props>('kitn-loader', {
20
+ variant: 'circular',
21
+ size: 'md',
22
+ text: undefined,
23
+ }, (props) => (
24
+ <Loader variant={props.variant} size={props.size} text={props.text} />
25
+ ));
@@ -0,0 +1,38 @@
1
+ import { defineKitnElement } from './define';
2
+ import { Markdown } from '../components/markdown';
3
+ import { ChatConfig, useChatConfig, type ProseSize } from '../primitives/chat-config';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The markdown source to render. */
7
+ content: string;
8
+ /** Text/markdown sizing. */
9
+ proseSize?: ProseSize;
10
+ /** Shiki theme for fenced code blocks. */
11
+ codeTheme?: string;
12
+ /** Disable syntax highlighting (no Shiki loads). */
13
+ codeHighlight?: boolean;
14
+ }
15
+
16
+ /**
17
+ * `<kitn-markdown>` — renders markdown (with fenced-code syntax highlighting) as
18
+ * a standalone element. Content via the `content` property; sizing/highlighting
19
+ * via attributes.
20
+ */
21
+ defineKitnElement<Props>('kitn-markdown', {
22
+ content: '',
23
+ proseSize: 'sm',
24
+ codeTheme: 'github-dark-dimmed',
25
+ codeHighlight: true,
26
+ }, (props, { flag }) => {
27
+ const outer = useChatConfig();
28
+ return (
29
+ <ChatConfig
30
+ proseSize={props.proseSize}
31
+ codeTheme={props.codeTheme}
32
+ codeHighlight={flag('codeHighlight')}
33
+ portalMount={outer.portalMount()}
34
+ >
35
+ <Markdown content={props.content} />
36
+ </ChatConfig>
37
+ );
38
+ });
@@ -0,0 +1,22 @@
1
+ import { defineKitnElement } from './define';
2
+ import { MessageSkills } from '../components/message-skills';
3
+
4
+ interface Skill {
5
+ /** Stable identifier for the skill. */
6
+ id: string;
7
+ /** Human-readable skill name shown on the badge. */
8
+ name: string;
9
+ }
10
+
11
+ interface Props extends Record<string, unknown> {
12
+ /** The active skills to badge. Set as a JS property. */
13
+ skills: Skill[];
14
+ }
15
+
16
+ /**
17
+ * `<kitn-message-skills>` — badges showing which skills were active for a
18
+ * message. Data via the `skills` property.
19
+ */
20
+ defineKitnElement<Props>('kitn-message-skills', {
21
+ skills: [],
22
+ }, (props) => <MessageSkills skills={props.skills} />);
@@ -0,0 +1,125 @@
1
+ import { For, Show, type Component } from 'solid-js';
2
+ import { defineKitnElement } from './define';
3
+ import { ChatConfig, useChatConfig, type ProseSize } from '../primitives/chat-config';
4
+ import { Message, MessageContent, MessageActions } from '../components/message';
5
+ import { Reasoning, ReasoningTrigger, ReasoningContent } from '../components/reasoning';
6
+ import { Tool } from '../components/tool';
7
+ import { Attachments, Attachment, AttachmentPreview, AttachmentInfo } from '../components/attachments';
8
+ import { Button } from '../ui/button';
9
+ import { Copy, ThumbsUp, ThumbsDown, RefreshCw, Pencil } from 'lucide-solid';
10
+ import type { ChatMessage, ChatMessageAction } from './chat-types';
11
+
12
+ interface Props extends Record<string, unknown> {
13
+ /** The full message object. Set as a JS property. */
14
+ message?: ChatMessage;
15
+ /** Convenience for simple cases when not passing a `message` object. */
16
+ role?: 'user' | 'assistant';
17
+ /** Convenience content (used when `message` is not set). */
18
+ content?: string;
19
+ /** Force markdown on/off. Defaults to on for assistant, off for user. */
20
+ markdown?: boolean;
21
+ /** Text/markdown sizing for the message body. */
22
+ proseSize?: ProseSize;
23
+ /** Shiki theme name used for fenced code blocks in the content. */
24
+ codeTheme?: string;
25
+ /** Disable syntax highlighting for code blocks (no Shiki loads). */
26
+ codeHighlight?: boolean;
27
+ }
28
+
29
+ /** Events fired by `<kitn-message>`. */
30
+ interface Events {
31
+ /** An action button was clicked. */
32
+ messageaction: { messageId: string; action: ChatMessageAction };
33
+ }
34
+
35
+ const ACTION_LABEL: Record<ChatMessageAction, string> = {
36
+ copy: 'Copy', like: 'Like', dislike: 'Dislike', regenerate: 'Regenerate', edit: 'Edit',
37
+ };
38
+ const ACTION_ICON: Record<ChatMessageAction, Component<{ class?: string }>> = {
39
+ copy: Copy, like: ThumbsUp, dislike: ThumbsDown, regenerate: RefreshCw, edit: Pencil,
40
+ };
41
+
42
+ /**
43
+ * `<kitn-message>` — a single message row: markdown/plain content, reasoning,
44
+ * tool calls, attachments, and action buttons, rendered from one `message`
45
+ * object (the same shape `<kitn-chat>` uses per message). The keystone of the
46
+ * "compose your own message list" pattern. Emits `messageaction`.
47
+ */
48
+ defineKitnElement<Props, Events>('kitn-message', {
49
+ message: undefined,
50
+ role: 'assistant',
51
+ content: undefined,
52
+ markdown: undefined,
53
+ proseSize: 'sm',
54
+ codeTheme: 'github-dark-dimmed',
55
+ codeHighlight: true,
56
+ }, (props, { dispatch, flag, element }) => {
57
+ const outer = useChatConfig();
58
+ const msg = (): ChatMessage =>
59
+ props.message ?? { id: 'message', role: props.role ?? 'assistant', content: props.content ?? '' };
60
+ const isUser = () => msg().role === 'user';
61
+ // markdown: explicit prop/attribute wins; otherwise default by role.
62
+ const markdownExplicit = () =>
63
+ element.hasAttribute('markdown') || props.markdown === true || props.markdown === false;
64
+ const useMarkdown = () => (markdownExplicit() ? flag('markdown') : !isUser());
65
+
66
+ return (
67
+ <ChatConfig
68
+ proseSize={props.proseSize}
69
+ codeTheme={props.codeTheme}
70
+ codeHighlight={flag('codeHighlight')}
71
+ portalMount={outer.portalMount()}
72
+ >
73
+ <Message class={isUser() ? 'flex-col items-end' : 'flex-col items-start'}>
74
+ <Show when={msg().reasoning}>
75
+ <Reasoning class="mb-2 w-full">
76
+ <ReasoningTrigger>{msg().reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
77
+ <ReasoningContent markdown>{msg().reasoning!.text}</ReasoningContent>
78
+ </Reasoning>
79
+ </Show>
80
+ <For each={msg().tools ?? []}>
81
+ {(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}
82
+ </For>
83
+ <Show when={msg().attachments?.length}>
84
+ <Attachments variant="inline" class={isUser() ? 'mb-2 justify-end' : 'mb-2'}>
85
+ <For each={msg().attachments!}>
86
+ {(att) => (
87
+ <Attachment data={att}>
88
+ <AttachmentPreview />
89
+ <AttachmentInfo />
90
+ </Attachment>
91
+ )}
92
+ </For>
93
+ </Attachments>
94
+ </Show>
95
+ <MessageContent
96
+ markdown={useMarkdown()}
97
+ class={isUser()
98
+ ? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2'
99
+ : 'bg-transparent p-0'}
100
+ >
101
+ {msg().content}
102
+ </MessageContent>
103
+ <Show when={msg().actions?.length}>
104
+ <MessageActions class="mt-1 flex gap-0">
105
+ <For each={msg().actions!}>
106
+ {(a) => (
107
+ <Button
108
+ variant="ghost" size="icon-sm" class="rounded-full"
109
+ data-action={a}
110
+ aria-label={ACTION_LABEL[a]}
111
+ onClick={() => dispatch('messageaction', { messageId: msg().id, action: a })}
112
+ >
113
+ {(() => {
114
+ const Icon = ACTION_ICON[a];
115
+ return <Icon class="size-3.5" />;
116
+ })()}
117
+ </Button>
118
+ )}
119
+ </For>
120
+ </MessageActions>
121
+ </Show>
122
+ </Message>
123
+ </ChatConfig>
124
+ );
125
+ });
@@ -0,0 +1,35 @@
1
+ import { defineKitnElement } from './define';
2
+ import { ModelSwitcher } from '../components/model-switcher';
3
+ import type { ModelOption } from '../types';
4
+
5
+ interface Props extends Record<string, unknown> {
6
+ /** The selectable models. Set as a JS property (array). */
7
+ models: ModelOption[];
8
+ /** The currently-selected model id. Defaults to the first model. */
9
+ currentModel?: string;
10
+ }
11
+
12
+ /** Events fired by `<kitn-model-switcher>`. */
13
+ interface Events {
14
+ /** A model was selected. */
15
+ modelchange: { modelId: string };
16
+ }
17
+
18
+ /**
19
+ * `<kitn-model-switcher>` — an event-emitting leaf element. Data in via the
20
+ * `models` property, selection out via a `modelchange` event. Mirrors the
21
+ * header switcher inside `<kitn-chat>` as a standalone, composable piece.
22
+ *
23
+ * Note: like the underlying primitive, this only renders when more than one
24
+ * model is provided.
25
+ */
26
+ defineKitnElement<Props, Events>('kitn-model-switcher', {
27
+ models: [],
28
+ currentModel: undefined,
29
+ }, (props, { dispatch }) => (
30
+ <ModelSwitcher
31
+ models={props.models}
32
+ currentModelId={props.currentModel ?? props.models[0]?.id ?? ''}
33
+ onModelChange={(modelId) => dispatch('modelchange', { modelId })}
34
+ />
35
+ ));