@kitn.ai/ui 0.14.1
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 +394 -0
- package/dist/bash-InADTalH.js +1 -0
- package/dist/core-AYMC6_lb.js +12 -0
- package/dist/css-M7EaDHN_.js +1 -0
- package/dist/custom-elements.json +4739 -0
- package/dist/engine-javascript-vq0WuIJl.js +141 -0
- package/dist/github-dark-dimmed-DUshB20C.js +1 -0
- package/dist/github-light-JYsPkUQd.js +1 -0
- package/dist/host-embed.d.ts +33 -0
- package/dist/html-CPZ3oZQ7.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/javascript-C25yR2R2.js +1 -0
- package/dist/json-DxJze_jm.js +1 -0
- package/dist/kitn-chat-provider.d.ts +2 -0
- package/dist/kitn-chat-provider.es.js +240 -0
- package/dist/kitn-chat.es.js +77 -0
- package/dist/llms/llms-full.txt +1018 -0
- package/dist/llms/llms.txt +104 -0
- package/dist/origin.d.ts +5 -0
- package/dist/provider-runtime.d.ts +27 -0
- package/dist/schemas/card-envelope.schema.json +14 -0
- package/dist/schemas/card-event.schema.json +12 -0
- package/dist/schemas/choice.schema.json +70 -0
- package/dist/schemas/confirm.schema.json +65 -0
- package/dist/schemas/embed.schema.json +65 -0
- package/dist/schemas/form.result.schema.json +7 -0
- package/dist/schemas/form.schema.json +33 -0
- package/dist/schemas/link.schema.json +56 -0
- package/dist/schemas/tasks.result.schema.json +16 -0
- package/dist/schemas/tasks.schema.json +78 -0
- package/dist/svelte--5p79yCD.js +1 -0
- package/dist/theme.tokens.css +176 -0
- package/dist/tsx-B8rCNbgL.js +1 -0
- package/dist/typescript-RycA9KXf.js +1 -0
- package/dist/validate.d.ts +4 -0
- package/dist/version.d.ts +4 -0
- package/dist/vue-BmIZj4XD.js +1 -0
- package/dist/wire.d.ts +53 -0
- package/frameworks/react/index.tsx +948 -0
- package/frameworks/react/runtime.tsx +94 -0
- package/llms-full.txt +1018 -0
- package/llms.txt +104 -0
- package/package.json +141 -0
- package/src/components/artifact.stories.tsx +138 -0
- package/src/components/artifact.tsx +743 -0
- package/src/components/attachments.stories.tsx +303 -0
- package/src/components/attachments.test.tsx +64 -0
- package/src/components/attachments.tsx +399 -0
- package/src/components/card-fallback.tsx +28 -0
- package/src/components/card-renderer.tsx +52 -0
- package/src/components/card.tsx +110 -0
- package/src/components/chain-of-thought.stories.tsx +211 -0
- package/src/components/chain-of-thought.tsx +139 -0
- package/src/components/chat-container.stories.tsx +187 -0
- package/src/components/chat-container.tsx +82 -0
- package/src/components/chat-scope-picker.tsx +47 -0
- package/src/components/chat-thread.test.tsx +58 -0
- package/src/components/chat-thread.tsx +249 -0
- package/src/components/checkpoint.stories.tsx +102 -0
- package/src/components/checkpoint.tsx +88 -0
- package/src/components/choice-card.tsx +569 -0
- package/src/components/code-block.stories.tsx +150 -0
- package/src/components/code-block.tsx +105 -0
- package/src/components/component-meta.json +3735 -0
- package/src/components/confirm-card.stories.tsx +74 -0
- package/src/components/confirm-card.tsx +315 -0
- package/src/components/context.stories.tsx +229 -0
- package/src/components/context.test.tsx +133 -0
- package/src/components/context.tsx +363 -0
- package/src/components/conversation-item.stories.tsx +125 -0
- package/src/components/conversation-item.tsx +16 -0
- package/src/components/conversation-list.stories.tsx +133 -0
- package/src/components/conversation-list.tsx +101 -0
- package/src/components/default-input.test.tsx +111 -0
- package/src/components/embed.tsx +196 -0
- package/src/components/empty.stories.tsx +434 -0
- package/src/components/empty.tsx +166 -0
- package/src/components/feedback-bar.stories.tsx +138 -0
- package/src/components/feedback-bar.test.tsx +103 -0
- package/src/components/feedback-bar.tsx +166 -0
- package/src/components/file-tree.stories.tsx +73 -0
- package/src/components/file-tree.tsx +383 -0
- package/src/components/file-upload.stories.tsx +156 -0
- package/src/components/file-upload.tsx +161 -0
- package/src/components/form-widgets.tsx +461 -0
- package/src/components/form.tsx +873 -0
- package/src/components/image.stories.tsx +89 -0
- package/src/components/image.tsx +67 -0
- package/src/components/link-preview.tsx +194 -0
- package/src/components/loader.stories.tsx +181 -0
- package/src/components/loader.tsx +333 -0
- package/src/components/markdown.stories.tsx +180 -0
- package/src/components/markdown.tsx +81 -0
- package/src/components/message-action-bar.test.tsx +128 -0
- package/src/components/message-narrow.stories.tsx +329 -0
- package/src/components/message-skills.stories.tsx +211 -0
- package/src/components/message-skills.tsx +36 -0
- package/src/components/message.stories.tsx +281 -0
- package/src/components/message.tsx +227 -0
- package/src/components/model-switcher.stories.tsx +123 -0
- package/src/components/model-switcher.test.tsx +65 -0
- package/src/components/model-switcher.tsx +87 -0
- package/src/components/prompt-input.stories.tsx +222 -0
- package/src/components/prompt-input.tsx +211 -0
- package/src/components/prompt-suggestion.stories.tsx +142 -0
- package/src/components/prompt-suggestion.tsx +115 -0
- package/src/components/reasoning.stories.tsx +140 -0
- package/src/components/reasoning.tsx +156 -0
- package/src/components/resizable.test.tsx +226 -0
- package/src/components/response-stream.tsx +103 -0
- package/src/components/scroll-button.stories.tsx +100 -0
- package/src/components/scroll-button.tsx +34 -0
- package/src/components/slash-command.stories.tsx +163 -0
- package/src/components/slash-command.tsx +232 -0
- package/src/components/source-list.test.tsx +152 -0
- package/src/components/source.stories.tsx +124 -0
- package/src/components/source.tsx +129 -0
- package/src/components/tasks-card.stories.tsx +78 -0
- package/src/components/tasks-card.tsx +432 -0
- package/src/components/text-shimmer.stories.tsx +87 -0
- package/src/components/text-shimmer.tsx +37 -0
- package/src/components/thinking-bar.stories.tsx +87 -0
- package/src/components/thinking-bar.tsx +50 -0
- package/src/components/tool.stories.tsx +153 -0
- package/src/components/tool.tsx +184 -0
- package/src/components/use-card-resolution.ts +28 -0
- package/src/components/voice-input.stories.tsx +83 -0
- package/src/components/voice-input.tsx +107 -0
- package/src/elements/artifact.stories.tsx +506 -0
- package/src/elements/artifact.tsx +137 -0
- package/src/elements/attachments.stories.tsx +190 -0
- package/src/elements/attachments.tsx +136 -0
- package/src/elements/card.stories.tsx +113 -0
- package/src/elements/card.tsx +40 -0
- package/src/elements/cards.stories.tsx +59 -0
- package/src/elements/cards.tsx +97 -0
- package/src/elements/catalog.stories.tsx +491 -0
- package/src/elements/chain-of-thought.declarative.test.tsx +153 -0
- package/src/elements/chain-of-thought.stories.tsx +113 -0
- package/src/elements/chain-of-thought.tsx +92 -0
- package/src/elements/chat-scope-picker.stories.tsx +73 -0
- package/src/elements/chat-scope-picker.tsx +36 -0
- package/src/elements/chat-types.ts +42 -0
- package/src/elements/chat-workspace.stories.tsx +288 -0
- package/src/elements/chat-workspace.tsx +161 -0
- package/src/elements/chat.stories.tsx +151 -0
- package/src/elements/chat.tsx +80 -0
- package/src/elements/checkpoint.stories.tsx +72 -0
- package/src/elements/checkpoint.tsx +47 -0
- package/src/elements/choice.stories.tsx +240 -0
- package/src/elements/choice.tsx +48 -0
- package/src/elements/code-block.stories.tsx +83 -0
- package/src/elements/code-block.tsx +42 -0
- package/src/elements/compiled.css +2 -0
- package/src/elements/composed-shell.stories.tsx +316 -0
- package/src/elements/confirm-card.stories.tsx +227 -0
- package/src/elements/confirm-card.tsx +51 -0
- package/src/elements/context-meter.stories.tsx +151 -0
- package/src/elements/context-meter.tsx +121 -0
- package/src/elements/conversation-list.declarative.test.tsx +137 -0
- package/src/elements/conversation-list.stories.tsx +243 -0
- package/src/elements/conversation-list.tsx +80 -0
- package/src/elements/css.ts +5 -0
- package/src/elements/default-input.tsx +247 -0
- package/src/elements/define.tsx +171 -0
- package/src/elements/element-meta.json +3314 -0
- package/src/elements/element-types.d.ts +710 -0
- package/src/elements/embed.stories.tsx +213 -0
- package/src/elements/embed.tsx +35 -0
- package/src/elements/empty.stories.tsx +111 -0
- package/src/elements/empty.tsx +29 -0
- package/src/elements/feedback-bar.stories.tsx +114 -0
- package/src/elements/feedback-bar.tsx +61 -0
- package/src/elements/file-tree.stories.tsx +134 -0
- package/src/elements/file-tree.tsx +52 -0
- package/src/elements/file-upload.stories.tsx +82 -0
- package/src/elements/file-upload.tsx +44 -0
- package/src/elements/form.stories.tsx +248 -0
- package/src/elements/form.tsx +43 -0
- package/src/elements/framework-usage.json +563 -0
- package/src/elements/image.stories.tsx +71 -0
- package/src/elements/image.tsx +32 -0
- package/src/elements/link-preview.stories.tsx +209 -0
- package/src/elements/link-preview.tsx +34 -0
- package/src/elements/loader.stories.tsx +88 -0
- package/src/elements/loader.tsx +25 -0
- package/src/elements/markdown.stories.tsx +76 -0
- package/src/elements/markdown.tsx +38 -0
- package/src/elements/message-skills.declarative.test.tsx +104 -0
- package/src/elements/message-skills.stories.tsx +114 -0
- package/src/elements/message-skills.tsx +85 -0
- package/src/elements/message.stories.tsx +218 -0
- package/src/elements/message.tsx +162 -0
- package/src/elements/model-switcher.declarative.test.tsx +130 -0
- package/src/elements/model-switcher.stories.tsx +198 -0
- package/src/elements/model-switcher.tsx +93 -0
- package/src/elements/popover.stories.tsx +80 -0
- package/src/elements/popover.tsx +59 -0
- package/src/elements/prompt-input-slash-command.test.tsx +128 -0
- package/src/elements/prompt-input-stoppable.test.tsx +88 -0
- package/src/elements/prompt-input-toolbar-actions.test.tsx +120 -0
- package/src/elements/prompt-input.stories.tsx +331 -0
- package/src/elements/prompt-input.tsx +185 -0
- package/src/elements/prompt-suggestions.declarative.test.tsx +164 -0
- package/src/elements/prompt-suggestions.stories.tsx +213 -0
- package/src/elements/prompt-suggestions.tsx +94 -0
- package/src/elements/reasoning.stories.tsx +77 -0
- package/src/elements/reasoning.tsx +50 -0
- package/src/elements/register.ts +56 -0
- package/src/elements/remote.stories.tsx +314 -0
- package/src/elements/remote.tsx +221 -0
- package/src/elements/resizable.d.ts +27 -0
- package/src/elements/resizable.stories.tsx +426 -0
- package/src/elements/resizable.tsx +553 -0
- package/src/elements/response-stream.stories.tsx +79 -0
- package/src/elements/response-stream.tsx +40 -0
- package/src/elements/scroll-button.stories.tsx +202 -0
- package/src/elements/scroll-button.test.tsx +148 -0
- package/src/elements/scroll-button.tsx +126 -0
- package/src/elements/source-list.stories.tsx +143 -0
- package/src/elements/source.stories.tsx +88 -0
- package/src/elements/source.tsx +120 -0
- package/src/elements/styles.css +199 -0
- package/src/elements/switch.stories.tsx +56 -0
- package/src/elements/switch.tsx +43 -0
- package/src/elements/tasks.stories.tsx +237 -0
- package/src/elements/tasks.tsx +46 -0
- package/src/elements/text-shimmer.stories.tsx +64 -0
- package/src/elements/text-shimmer.tsx +28 -0
- package/src/elements/thinking-bar.stories.tsx +72 -0
- package/src/elements/thinking-bar.tsx +34 -0
- package/src/elements/tool.stories.tsx +89 -0
- package/src/elements/tool.tsx +23 -0
- package/src/elements/voice-input.stories.tsx +88 -0
- package/src/elements/voice-input.tsx +41 -0
- package/src/index.ts +253 -0
- package/src/primitives/card-contract.ts +71 -0
- package/src/primitives/card-host.tsx +35 -0
- package/src/primitives/card-registry.tsx +67 -0
- package/src/primitives/card-resolution.ts +36 -0
- package/src/primitives/card-routing.ts +79 -0
- package/src/primitives/card-schemas/card-envelope.schema.json +14 -0
- package/src/primitives/card-schemas/card-event.schema.json +12 -0
- package/src/primitives/card-schemas/choice.schema.json +70 -0
- package/src/primitives/card-schemas/confirm.schema.json +65 -0
- package/src/primitives/card-schemas/embed.schema.json +65 -0
- package/src/primitives/card-schemas/form.result.schema.json +7 -0
- package/src/primitives/card-schemas/form.schema.json +33 -0
- package/src/primitives/card-schemas/link.schema.json +56 -0
- package/src/primitives/card-schemas/tasks.result.schema.json +16 -0
- package/src/primitives/card-schemas/tasks.schema.json +78 -0
- package/src/primitives/card-validate.ts +95 -0
- package/src/primitives/chat-config.tsx +76 -0
- package/src/primitives/controllable.test.ts +47 -0
- package/src/primitives/controllable.ts +21 -0
- package/src/primitives/embed-providers.ts +254 -0
- package/src/primitives/highlighter.ts +157 -0
- package/src/primitives/link-preview.ts +87 -0
- package/src/primitives/pdf-preview.ts +121 -0
- package/src/primitives/use-auto-resize.ts +31 -0
- package/src/primitives/use-resize-observer.ts +12 -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/remote/host-embed.ts +345 -0
- package/src/remote/index.ts +2 -0
- package/src/remote/origin.ts +30 -0
- package/src/remote/provider-runtime.ts +259 -0
- package/src/remote/provider.ts +2 -0
- package/src/remote/validate.ts +22 -0
- package/src/remote/version.ts +12 -0
- package/src/remote/wire.ts +48 -0
- package/src/stories/chat-panel-layout.stories.tsx +145 -0
- package/src/stories/chat-scene.tsx +571 -0
- package/src/stories/checkpoint-restore.stories.tsx +256 -0
- package/src/stories/context-usage.stories.tsx +212 -0
- package/src/stories/conversation-with-reasoning.stories.tsx +182 -0
- package/src/stories/conversation-with-sources.stories.tsx +471 -0
- package/src/stories/docs/Accessibility.mdx +119 -0
- package/src/stories/docs/ChildElements.mdx +269 -0
- package/src/stories/docs/ForAIAgents.mdx +162 -0
- package/src/stories/docs/GettingStarted.mdx +65 -0
- package/src/stories/docs/Installation.mdx +83 -0
- package/src/stories/docs/Introduction.mdx +50 -0
- package/src/stories/docs/SolidJsAdvanced.mdx +17 -0
- package/src/stories/docs/Theming.mdx +85 -0
- package/src/stories/docs/element-controls.ts +121 -0
- package/src/stories/docs/frameworks/Angular.mdx +245 -0
- package/src/stories/docs/frameworks/Html.mdx +227 -0
- package/src/stories/docs/frameworks/Overview.mdx +47 -0
- package/src/stories/docs/frameworks/React.mdx +227 -0
- package/src/stories/docs/frameworks/Solid.mdx +173 -0
- package/src/stories/docs/frameworks/Svelte.mdx +194 -0
- package/src/stories/docs/frameworks/Vue.mdx +238 -0
- package/src/stories/docs/generative-ui-overview.mdx +186 -0
- package/src/stories/docs/recipes/SpeechToText.mdx +7 -0
- package/src/stories/docs/recipes/Streaming.mdx +68 -0
- package/src/stories/docs/recipes/TextToSpeech.mdx +35 -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 +146 -0
- package/src/stories/docs/theme-tokens.tsx +174 -0
- package/src/stories/examples/ChoosingComponents.mdx +106 -0
- package/src/stories/examples/sample-data.ts +79 -0
- package/src/stories/examples/usage/checkpoint-restore.ts +156 -0
- package/src/stories/examples/usage/context-usage.ts +591 -0
- package/src/stories/examples/usage/conversation-with-reasoning.ts +224 -0
- package/src/stories/examples/usage/conversation-with-sources.ts +589 -0
- package/src/stories/examples/usage/empty-state.ts +144 -0
- package/src/stories/examples/usage/full-chat-app.ts +277 -0
- package/src/stories/examples/usage/index.ts +63 -0
- package/src/stories/examples/usage/message-actions.ts +704 -0
- package/src/stories/examples/usage/pattern-centered-conversation.ts +446 -0
- package/src/stories/examples/usage/pattern-chat-panel-layout.ts +442 -0
- package/src/stories/examples/usage/pattern-docked-widget.ts +479 -0
- package/src/stories/examples/usage/prompt-input-variants.ts +1356 -0
- package/src/stories/examples/usage/streaming-response.ts +687 -0
- package/src/stories/examples/usage/types.ts +33 -0
- package/src/stories/full-chat.stories.tsx +18 -0
- package/src/stories/message-actions.stories.tsx +230 -0
- package/src/stories/pattern-centered-conversation.stories.tsx +93 -0
- package/src/stories/pattern-docked-widget.stories.tsx +93 -0
- package/src/stories/pattern-empty-state.stories.tsx +76 -0
- package/src/stories/prompt-input-variants.stories.tsx +456 -0
- package/src/stories/streaming-response.stories.tsx +438 -0
- package/src/stories/theme-editor.stories.tsx +16 -0
- package/src/stories/token-reference.stories.tsx +18 -0
- package/src/stories/typography.stories.tsx +78 -0
- package/src/types.ts +47 -0
- package/src/ui/action-icons.ts +52 -0
- package/src/ui/avatar.stories.tsx +103 -0
- package/src/ui/avatar.tsx +23 -0
- package/src/ui/badge.stories.tsx +86 -0
- package/src/ui/badge.tsx +21 -0
- package/src/ui/button.stories.tsx +145 -0
- package/src/ui/button.tsx +38 -0
- package/src/ui/collapsible.stories.tsx +69 -0
- package/src/ui/collapsible.tsx +125 -0
- package/src/ui/dropdown.stories.tsx +59 -0
- package/src/ui/dropdown.tsx +196 -0
- package/src/ui/hover-card.stories.tsx +77 -0
- package/src/ui/hover-card.test.tsx +30 -0
- package/src/ui/hover-card.tsx +172 -0
- package/src/ui/overlay.stories.tsx +115 -0
- package/src/ui/overlay.tsx +158 -0
- package/src/ui/popover.stories.tsx +81 -0
- package/src/ui/popover.test.tsx +99 -0
- package/src/ui/popover.tsx +92 -0
- package/src/ui/resizable.stories.tsx +236 -0
- package/src/ui/resizable.tsx +576 -0
- package/src/ui/scroll-area.stories.tsx +50 -0
- package/src/ui/scroll-area.tsx +15 -0
- package/src/ui/separator.stories.tsx +81 -0
- package/src/ui/separator.tsx +10 -0
- package/src/ui/skeleton.stories.tsx +337 -0
- package/src/ui/skeleton.tsx +16 -0
- package/src/ui/switch.stories.tsx +51 -0
- package/src/ui/switch.test.tsx +59 -0
- package/src/ui/switch.tsx +62 -0
- package/src/ui/textarea.stories.tsx +76 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/tooltip.stories.tsx +74 -0
- package/src/ui/tooltip.tsx +68 -0
- package/src/utils/cn.ts +24 -0
- package/theme.css +187 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the declarative `<kai-skill>` light-DOM API of `<kai-skills>`.
|
|
3
|
+
*
|
|
4
|
+
* Strategy: the `defineWebComponent` call registers a real Shadow-DOM custom
|
|
5
|
+
* element which requires a full browser environment (Constructable Stylesheets,
|
|
6
|
+
* shadow roots, etc.) and is therefore not suitable for jsdom unit tests.
|
|
7
|
+
* Instead we:
|
|
8
|
+
* 1. Test the exported `parseKaiSkillElement` helper in isolation — it has no
|
|
9
|
+
* DOM dependencies beyond `Element`, which jsdom provides perfectly.
|
|
10
|
+
* 2. Test that the merged list of prop + slotted items renders badge spans
|
|
11
|
+
* correctly via `MessageSkills` directly, mirroring the pattern used in
|
|
12
|
+
* `prompt-suggestions.declarative.test.tsx`.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
15
|
+
import '@testing-library/jest-dom/vitest';
|
|
16
|
+
import { render, cleanup } from '@solidjs/testing-library';
|
|
17
|
+
import { For } from 'solid-js';
|
|
18
|
+
import { parseKaiSkillElement } from './message-skills';
|
|
19
|
+
import { MessageSkills } from '../components/message-skills';
|
|
20
|
+
|
|
21
|
+
afterEach(cleanup);
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// parseKaiSkillElement — pure helper
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
describe('parseKaiSkillElement', () => {
|
|
28
|
+
function makeNode(textContent: string, id?: string): Element {
|
|
29
|
+
const el = document.createElement('kai-skill');
|
|
30
|
+
el.textContent = textContent;
|
|
31
|
+
if (id !== undefined) el.setAttribute('id', id);
|
|
32
|
+
return el;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
it('uses textContent as name', () => {
|
|
36
|
+
const item = parseKaiSkillElement(makeNode('Web Search'));
|
|
37
|
+
expect(item).toMatchObject({ name: 'Web Search' });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('uses id attribute as id when present', () => {
|
|
41
|
+
const item = parseKaiSkillElement(makeNode('Web Search', 'web-search'));
|
|
42
|
+
expect(item).toMatchObject({ id: 'web-search', name: 'Web Search' });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('falls back to textContent as id when id attribute is absent', () => {
|
|
46
|
+
const item = parseKaiSkillElement(makeNode('Web Search'));
|
|
47
|
+
expect(item).toMatchObject({ id: 'Web Search', name: 'Web Search' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('trims leading/trailing whitespace from textContent', () => {
|
|
51
|
+
const item = parseKaiSkillElement(makeNode(' Code Interpreter '));
|
|
52
|
+
expect(item).toMatchObject({ name: 'Code Interpreter' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handles empty textContent with an id attribute', () => {
|
|
56
|
+
const item = parseKaiSkillElement(makeNode('', 'code'));
|
|
57
|
+
expect(item).toMatchObject({ id: 'code', name: '' });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Merge + render: prop skills + slotted skills combine correctly
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
interface Skill {
|
|
66
|
+
id: string;
|
|
67
|
+
name: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('MessageSkills rendering with merged skills', () => {
|
|
71
|
+
it('renders one badge span per skill', () => {
|
|
72
|
+
const { getByText } = render(() => (
|
|
73
|
+
<MessageSkills skills={[{ id: 'web-search', name: 'Web Search' }, { id: 'code', name: 'Code' }]} />
|
|
74
|
+
));
|
|
75
|
+
expect(getByText('Web Search')).toBeInTheDocument();
|
|
76
|
+
expect(getByText('Code')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders nothing when skills is empty', () => {
|
|
80
|
+
const { container } = render(() => <MessageSkills skills={[]} />);
|
|
81
|
+
expect(container.firstChild).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('renders prop items before declarative (slotted) items', () => {
|
|
85
|
+
// Simulate the merge: prop first, slotted second.
|
|
86
|
+
const propSkills: Skill[] = [{ id: 'prop', name: 'Prop Skill' }];
|
|
87
|
+
const slottedSkills: Skill[] = [{ id: 'slotted', name: 'Slotted Skill' }];
|
|
88
|
+
const merged = [...propSkills, ...slottedSkills];
|
|
89
|
+
|
|
90
|
+
const { getAllByText } = render(() => <MessageSkills skills={merged} />);
|
|
91
|
+
// Both must be present; order is enforced by DOM order
|
|
92
|
+
expect(getAllByText(/Prop Skill|Slotted Skill/)).toHaveLength(2);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('parseKaiSkillElement produces items that render correctly', () => {
|
|
96
|
+
const el = document.createElement('kai-skill');
|
|
97
|
+
el.textContent = 'Memory';
|
|
98
|
+
el.setAttribute('id', 'memory');
|
|
99
|
+
|
|
100
|
+
const parsed = parseKaiSkillElement(el);
|
|
101
|
+
const { getByText } = render(() => <MessageSkills skills={[parsed]} />);
|
|
102
|
+
expect(getByText('Memory')).toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
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 { argTypesFor, specDescription } from '../stories/docs/element-controls';
|
|
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
|
+
'kai-skills': JSX.HTMLAttributes<HTMLElement>;
|
|
12
|
+
/** Light-DOM data carrier for declarative skill badges inside `<kai-skills>`. */
|
|
13
|
+
'kai-skill': JSX.HTMLAttributes<HTMLElement> & {
|
|
14
|
+
id?: string;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface Skill {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sampleSkills: Skill[] = [
|
|
26
|
+
{ id: 's1', name: 'web-search' },
|
|
27
|
+
{ id: 's2', name: 'code' },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Render the actual `<kai-skills>` custom element with a `skills` property. */
|
|
31
|
+
function MessageSkillsElement(props: { skills: Skill[] }) {
|
|
32
|
+
let el: (HTMLElement & { skills?: Skill[] }) | undefined;
|
|
33
|
+
onMount(() => {
|
|
34
|
+
if (el) el.skills = props.skills;
|
|
35
|
+
});
|
|
36
|
+
return (
|
|
37
|
+
<kai-skills ref={(e) => (el = e as HTMLElement)} style={{ display: 'block', padding: '16px' }} />
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
|
|
42
|
+
<kai-skills id="skills"></kai-skills>
|
|
43
|
+
|
|
44
|
+
<script type="module">
|
|
45
|
+
import '@kitn.ai/ui/elements'; // registers the custom elements
|
|
46
|
+
|
|
47
|
+
const skills = document.getElementById('skills');
|
|
48
|
+
skills.skills = [
|
|
49
|
+
{ id: 's1', name: 'web-search' },
|
|
50
|
+
{ id: 's2', name: 'code' },
|
|
51
|
+
];
|
|
52
|
+
</script>`;
|
|
53
|
+
|
|
54
|
+
const meta = {
|
|
55
|
+
title: 'Components/Skills',
|
|
56
|
+
tags: ['autodocs'],
|
|
57
|
+
argTypes: argTypesFor('kai-skills'),
|
|
58
|
+
parameters: {
|
|
59
|
+
layout: 'fullscreen',
|
|
60
|
+
docs: {
|
|
61
|
+
description: specDescription('kai-skills', [
|
|
62
|
+
'`<kai-skills>` is the framework-agnostic **web component** that badges which skills were active for a message, isolated in **Shadow DOM**.',
|
|
63
|
+
'**When to use:** annotating a message row with the skills/tools it used, in a non-Solid app. In SolidJS, use the `MessageSkills` primitive directly.',
|
|
64
|
+
'**Placement:** beneath or inline with the message content, typically in the footer of a message row alongside action buttons; it is `display: block` and wraps badges horizontally.',
|
|
65
|
+
"**How to use:** register once with `import '@kitn.ai/ui/elements'`. Then either (a) set the `skills` **property** to an array of `{ id, name }` objects, or (b) compose `<kai-skill>` child elements — light-DOM data carriers hidden by the Shadow DOM — where the tag's text content is the badge label and the optional `id` attribute is the stable identifier. When both are provided, prop items render first and declarative children are appended after.",
|
|
66
|
+
'See the **Code** tab for HTML usage.',
|
|
67
|
+
]),
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
} satisfies Meta;
|
|
71
|
+
|
|
72
|
+
export default meta;
|
|
73
|
+
type Story = StoryObj;
|
|
74
|
+
|
|
75
|
+
/** Two active-skill badges set via the `skills` JS property. */
|
|
76
|
+
export const Default: Story = {
|
|
77
|
+
render: () => <MessageSkillsElement skills={sampleSkills} />,
|
|
78
|
+
parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const DECLARATIVE_HTML_SNIPPET = `<!-- Works in any framework or plain HTML — no JS property assignment needed -->
|
|
82
|
+
<kai-skills>
|
|
83
|
+
<kai-skill id="web-search">Web Search</kai-skill>
|
|
84
|
+
<kai-skill id="code">Code</kai-skill>
|
|
85
|
+
<kai-skill id="memory">Memory</kai-skill>
|
|
86
|
+
</kai-skills>
|
|
87
|
+
|
|
88
|
+
<script type="module">
|
|
89
|
+
import '@kitn.ai/ui/elements'; // registers the custom elements
|
|
90
|
+
</script>`;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Declare each skill badge as a `<kai-skill>` child element — no `skills`
|
|
94
|
+
* property or JS array wiring needed. The tag's text content is the badge
|
|
95
|
+
* label; the optional `id` attribute is the stable identifier (falls back to
|
|
96
|
+
* the label when absent). Children are light-DOM data carriers hidden by the
|
|
97
|
+
* Shadow DOM — pure data, no visible output of their own.
|
|
98
|
+
* Mix with the `skills` prop: prop items render first, declarative children after.
|
|
99
|
+
*/
|
|
100
|
+
export const DeclarativeSkills: Story = {
|
|
101
|
+
name: 'Declarative Skills (kai-skill)',
|
|
102
|
+
render: () => (
|
|
103
|
+
<kai-skills style={{ display: 'block', padding: '16px' }}>
|
|
104
|
+
<kai-skill id="web-search">Web Search</kai-skill>
|
|
105
|
+
<kai-skill id="code">Code</kai-skill>
|
|
106
|
+
<kai-skill id="memory">Memory</kai-skill>
|
|
107
|
+
</kai-skills>
|
|
108
|
+
),
|
|
109
|
+
parameters: {
|
|
110
|
+
docs: {
|
|
111
|
+
source: { code: DECLARATIVE_HTML_SNIPPET, language: 'html' },
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createSignal, onCleanup, onMount } from 'solid-js';
|
|
2
|
+
import { defineWebComponent } from './define';
|
|
3
|
+
import { MessageSkills } from '../components/message-skills';
|
|
4
|
+
|
|
5
|
+
interface Skill {
|
|
6
|
+
/** Stable identifier for the skill. */
|
|
7
|
+
id: string;
|
|
8
|
+
/** Human-readable skill name shown on the badge. */
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Props extends Record<string, unknown> {
|
|
13
|
+
/** The active skills to badge. Set as a JS property. */
|
|
14
|
+
skills: Skill[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a single light-DOM `<kai-skill>` element into a `Skill` descriptor.
|
|
19
|
+
*
|
|
20
|
+
* Attribute / content mapping:
|
|
21
|
+
* - `id` → Skill.id (falls back to `name` when absent)
|
|
22
|
+
* - `textContent` → Skill.name (the human-readable badge label)
|
|
23
|
+
*
|
|
24
|
+
* Example: `<kai-skill id="web-search">Web Search</kai-skill>`
|
|
25
|
+
*/
|
|
26
|
+
export function parseKaiSkillElement(n: Element): Skill {
|
|
27
|
+
const name = n.textContent?.trim() ?? '';
|
|
28
|
+
const id = n.getAttribute('id') ?? name;
|
|
29
|
+
return { id, name };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `<kai-skills>` — badges showing which skills were active for a
|
|
34
|
+
* message. Data via the `skills` property **or** declarative `<kai-skill>`
|
|
35
|
+
* children.
|
|
36
|
+
*
|
|
37
|
+
* **Property API** — set a JS array on the element:
|
|
38
|
+
* ```html
|
|
39
|
+
* <kai-skills id="skills"></kai-skills>
|
|
40
|
+
* <script type="module">
|
|
41
|
+
* import '@kitn.ai/ui/elements';
|
|
42
|
+
* document.getElementById('skills').skills = [
|
|
43
|
+
* { id: 'web-search', name: 'Web Search' },
|
|
44
|
+
* { id: 'code', name: 'Code' },
|
|
45
|
+
* ];
|
|
46
|
+
* </script>
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* **Declarative API** — compose `<kai-skill>` children (light-DOM data
|
|
50
|
+
* carriers hidden by Shadow DOM — no visible output of their own):
|
|
51
|
+
* ```html
|
|
52
|
+
* <kai-skills>
|
|
53
|
+
* <kai-skill id="web-search">Web Search</kai-skill>
|
|
54
|
+
* <kai-skill id="code">Code</kai-skill>
|
|
55
|
+
* </kai-skills>
|
|
56
|
+
* <script type="module">
|
|
57
|
+
* import '@kitn.ai/ui/elements';
|
|
58
|
+
* </script>
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* When both are provided, `skills` prop items render first and declarative
|
|
62
|
+
* children are appended after.
|
|
63
|
+
*/
|
|
64
|
+
defineWebComponent<Props>('kai-skills', {
|
|
65
|
+
skills: [],
|
|
66
|
+
}, (props, { element }) => {
|
|
67
|
+
// Read declarative <kai-skill> children from light DOM.
|
|
68
|
+
// The shadow root has no <slot>, so they are invisible — pure data carriers.
|
|
69
|
+
const [slottedSkills, setSlottedSkills] = createSignal<Skill[]>([]);
|
|
70
|
+
onMount(() => {
|
|
71
|
+
const read = () => {
|
|
72
|
+
const nodes = [...element.querySelectorAll('kai-skill')];
|
|
73
|
+
setSlottedSkills(nodes.map(parseKaiSkillElement));
|
|
74
|
+
};
|
|
75
|
+
read();
|
|
76
|
+
const observer = new MutationObserver(read);
|
|
77
|
+
observer.observe(element, { childList: true, attributes: true, subtree: true });
|
|
78
|
+
onCleanup(() => observer.disconnect());
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Prop skills (first) merged with declarative children (after).
|
|
82
|
+
const allSkills = () => [...(props.skills ?? []), ...slottedSkills()];
|
|
83
|
+
|
|
84
|
+
return <MessageSkills skills={allSkills()} />;
|
|
85
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
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 { ChatMessage } from './chat-types';
|
|
5
|
+
import { argTypesFor, specDescription } from '../stories/docs/element-controls';
|
|
6
|
+
|
|
7
|
+
// The web components are custom DOM elements, so declare the tags for JSX.
|
|
8
|
+
declare module 'solid-js' {
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
10
|
+
namespace JSX {
|
|
11
|
+
interface IntrinsicElements {
|
|
12
|
+
'kai-message': JSX.HTMLAttributes<HTMLElement>;
|
|
13
|
+
'kai-action': JSX.HTMLAttributes<HTMLElement> & { icon?: string; tooltip?: string };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const assistantMessage: ChatMessage = {
|
|
19
|
+
id: 'm-a',
|
|
20
|
+
role: 'assistant',
|
|
21
|
+
content:
|
|
22
|
+
"Here's the plan, with a quick code sample:\n\n```js\nconst kit = useChat();\n```\n\nThat wires the kit into your component.",
|
|
23
|
+
reasoning: { text: 'The user wants X, so I should do Y then Z.', label: 'Reasoning' },
|
|
24
|
+
tools: [{ type: 'search', state: 'output-available', input: { query: 'kitn docs' }, output: { hits: 3 } }],
|
|
25
|
+
attachments: [
|
|
26
|
+
{
|
|
27
|
+
id: '1',
|
|
28
|
+
type: 'file',
|
|
29
|
+
filename: 'architecture.png',
|
|
30
|
+
mediaType: 'image/png',
|
|
31
|
+
url:
|
|
32
|
+
'data:image/svg+xml;utf8,' +
|
|
33
|
+
encodeURIComponent(
|
|
34
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96"><rect width="96" height="96" rx="12" fill="#7c3aed"/><text x="48" y="60" font-size="42" text-anchor="middle" fill="white">◆</text></svg>',
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
actions: ['copy', 'like', 'dislike', 'regenerate'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const userMessage: ChatMessage = {
|
|
42
|
+
id: 'm-u',
|
|
43
|
+
role: 'user',
|
|
44
|
+
content: 'How do I compose these myself?',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const AVATAR_SVG =
|
|
48
|
+
'data:image/svg+xml;utf8,' +
|
|
49
|
+
encodeURIComponent(
|
|
50
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#10b981"/><text x="32" y="42" font-size="28" text-anchor="middle" fill="white">K</text></svg>',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/** An assistant message with an avatar (from `message.avatar`) plus custom + built-in actions. */
|
|
54
|
+
const avatarMessage: ChatMessage = {
|
|
55
|
+
id: 'm-av',
|
|
56
|
+
role: 'assistant',
|
|
57
|
+
content: 'I have an avatar, a custom **Share** action, and the built-in copy/like actions.',
|
|
58
|
+
avatar: { src: AVATAR_SVG, alt: 'Kitn', fallback: 'K' },
|
|
59
|
+
actions: ['copy', 'like', { id: 'share', label: 'Share', icon: 'share' }],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Render the actual `<kai-message>` custom element with a `message` property and optional attrs. */
|
|
63
|
+
function MessageElement(props: { message: ChatMessage; actionsReveal?: 'always' | 'hover' }) {
|
|
64
|
+
let el: (HTMLElement & { message?: ChatMessage }) | undefined;
|
|
65
|
+
onMount(() => {
|
|
66
|
+
if (!el) return;
|
|
67
|
+
if (props.actionsReveal) el.setAttribute('actions-reveal', props.actionsReveal);
|
|
68
|
+
el.message = props.message;
|
|
69
|
+
el.addEventListener('kai-message-action', (e) =>
|
|
70
|
+
console.log('kai-message-action', (e as CustomEvent).detail.action),
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
return (
|
|
74
|
+
<kai-message ref={(e) => (el = e as HTMLElement)} style={{ display: 'block', padding: '16px', 'max-width': '720px' }} />
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
|
|
79
|
+
<kai-message id="msg" style="display:block;"></kai-message>
|
|
80
|
+
|
|
81
|
+
<script type="module">
|
|
82
|
+
import '@kitn.ai/ui/elements'; // registers the custom elements
|
|
83
|
+
|
|
84
|
+
const msg = document.getElementById('msg');
|
|
85
|
+
msg.message = {
|
|
86
|
+
id: 'm-a', role: 'assistant',
|
|
87
|
+
content: "Here's the plan:\\n\\n\\\`\\\`\\\`js\\nconst kit = useChat();\\n\\\`\\\`\\\`",
|
|
88
|
+
reasoning: { text: 'The user wants X, so I should do Y.' },
|
|
89
|
+
tools: [{ type: 'search', state: 'output-available', output: { hits: 3 } }],
|
|
90
|
+
actions: ['copy', 'like', 'dislike', 'regenerate'],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// events are CustomEvents on the element (they do not bubble)
|
|
94
|
+
msg.addEventListener('kai-message-action', (e) => console.log(e.detail.action, e.detail.messageId));
|
|
95
|
+
</script>`;
|
|
96
|
+
|
|
97
|
+
const meta = {
|
|
98
|
+
title: 'Components/Message',
|
|
99
|
+
tags: ['autodocs'],
|
|
100
|
+
argTypes: argTypesFor('kai-message'),
|
|
101
|
+
parameters: {
|
|
102
|
+
layout: 'fullscreen',
|
|
103
|
+
docs: {
|
|
104
|
+
description: specDescription('kai-message', [
|
|
105
|
+
'`<kai-message>` is the framework-agnostic **web component** for a single message row — markdown/plain content, an optional reasoning block, tool calls, attachments, and action buttons — all rendered from one `message` object (the same shape `<kai-chat>` uses per message). It is the keystone of the "compose your own message list" pattern, isolated in **Shadow DOM**.',
|
|
106
|
+
"**When to use:** building a custom message thread in a non-Solid app, or anywhere you want to lay out the list yourself but keep the kit's rich message rendering. In SolidJS, compose the `Message` primitives for finer control.",
|
|
107
|
+
"**How to use:** register once with `import '@kitn.ai/ui/elements'`, set the whole row via the `message` **property** (`el.message = {...}`), and listen for the `kai-message-action` **CustomEvent** for action-button clicks. For simple cases, set `role` + `content` attributes instead of a full object.",
|
|
108
|
+
'See the **Code** tab for HTML usage.',
|
|
109
|
+
]),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
} satisfies Meta;
|
|
113
|
+
|
|
114
|
+
export default meta;
|
|
115
|
+
type Story = StoryObj;
|
|
116
|
+
|
|
117
|
+
/** A rich assistant message: markdown, reasoning, a tool call, an attachment, and actions. */
|
|
118
|
+
export const Assistant: Story = {
|
|
119
|
+
render: () => <MessageElement message={assistantMessage} />,
|
|
120
|
+
parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** A plain user message (markdown defaults off for the user role). */
|
|
124
|
+
export const User: Story = {
|
|
125
|
+
render: () => <MessageElement message={userMessage} />,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** An avatar (from `message.avatar`) laid out beside the content column, with a
|
|
129
|
+
* custom **Share** action (curated `icon`) alongside the built-in copy/like. */
|
|
130
|
+
export const WithAvatarAndCustomAction: Story = {
|
|
131
|
+
name: 'Avatar + Custom Action',
|
|
132
|
+
render: () => <MessageElement message={avatarMessage} />,
|
|
133
|
+
parameters: {
|
|
134
|
+
docs: {
|
|
135
|
+
source: {
|
|
136
|
+
code: `<kai-message id="msg" style="display:block;"></kai-message>
|
|
137
|
+
<script type="module">
|
|
138
|
+
import '@kitn.ai/ui/elements';
|
|
139
|
+
const msg = document.getElementById('msg');
|
|
140
|
+
msg.message = {
|
|
141
|
+
id: 'm-av', role: 'assistant', content: 'I have an avatar and a custom Share action.',
|
|
142
|
+
avatar: { src: '/k.png', alt: 'Kitn', fallback: 'K' },
|
|
143
|
+
// built-in names AND custom { id, label, icon } descriptors:
|
|
144
|
+
actions: ['copy', 'like', { id: 'share', label: 'Share', icon: 'share' }],
|
|
145
|
+
};
|
|
146
|
+
msg.addEventListener('kai-message-action', (e) => console.log(e.detail.action)); // 'copy' | 'like' | 'share'
|
|
147
|
+
</script>`,
|
|
148
|
+
language: 'html',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Declarative actions — `<kai-action>` light-DOM children instead of a
|
|
155
|
+
* `message.actions` array. Each carries `id`, a curated `icon`, an optional
|
|
156
|
+
* `tooltip`, and optional text (the accessible label). Great for plain HTML. */
|
|
157
|
+
export const DeclarativeActions: Story = {
|
|
158
|
+
name: 'Declarative Actions (kai-action)',
|
|
159
|
+
render: () => {
|
|
160
|
+
let el: HTMLElement | undefined;
|
|
161
|
+
onMount(() => {
|
|
162
|
+
if (!el) return;
|
|
163
|
+
el.setAttribute('avatar-fallback', 'AI');
|
|
164
|
+
el.setAttribute(
|
|
165
|
+
'content',
|
|
166
|
+
'Declare each button as a `<kai-action>` child — no `message` object or JS wiring needed.',
|
|
167
|
+
);
|
|
168
|
+
el.addEventListener('kai-message-action', (e) =>
|
|
169
|
+
console.log('kai-message-action', (e as CustomEvent).detail.action),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
return (
|
|
173
|
+
<kai-message
|
|
174
|
+
ref={(e) => (el = e as HTMLElement)}
|
|
175
|
+
style={{ display: 'block', padding: '16px', 'max-width': '720px' }}
|
|
176
|
+
>
|
|
177
|
+
<kai-action id="copy" icon="copy" tooltip="Copy"></kai-action>
|
|
178
|
+
<kai-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kai-action>
|
|
179
|
+
<kai-action id="share" icon="share" tooltip="Share">Share</kai-action>
|
|
180
|
+
<kai-action id="bookmark" icon="bookmark" tooltip="Bookmark">Bookmark</kai-action>
|
|
181
|
+
</kai-message>
|
|
182
|
+
);
|
|
183
|
+
},
|
|
184
|
+
parameters: {
|
|
185
|
+
docs: {
|
|
186
|
+
source: {
|
|
187
|
+
code: `<kai-message role="assistant" avatar-fallback="AI" content="..." style="display:block;">
|
|
188
|
+
<kai-action id="copy" icon="copy" tooltip="Copy"></kai-action>
|
|
189
|
+
<kai-action id="regenerate" icon="regenerate" tooltip="Regenerate"></kai-action>
|
|
190
|
+
<kai-action id="share" icon="share" tooltip="Share">Share</kai-action>
|
|
191
|
+
<kai-action id="bookmark" icon="bookmark" tooltip="Bookmark">Bookmark</kai-action>
|
|
192
|
+
</kai-message>
|
|
193
|
+
|
|
194
|
+
<script type="module">
|
|
195
|
+
import '@kitn.ai/ui/elements';
|
|
196
|
+
document.querySelector('kai-message')
|
|
197
|
+
.addEventListener('kai-message-action', (e) => console.log(e.detail.action)); // 'copy' | 'share' | …
|
|
198
|
+
</script>`,
|
|
199
|
+
language: 'html',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/** `actions-reveal="hover"` — the action bar is hidden until you hover the row. */
|
|
206
|
+
export const ActionsRevealOnHover: Story = {
|
|
207
|
+
name: 'Actions Reveal on Hover',
|
|
208
|
+
render: () => <MessageElement message={avatarMessage} actionsReveal="hover" />,
|
|
209
|
+
parameters: {
|
|
210
|
+
docs: {
|
|
211
|
+
source: {
|
|
212
|
+
code: `<!-- actions-reveal: 'always' (default) | 'hover' -->
|
|
213
|
+
<kai-message actions-reveal="hover" id="msg" style="display:block;"></kai-message>`,
|
|
214
|
+
language: 'html',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { For, Show, createSignal, onMount, onCleanup } from 'solid-js';
|
|
2
|
+
import { defineWebComponent } from './define';
|
|
3
|
+
import { ChatConfig, useChatConfig, type ProseSize } from '../primitives/chat-config';
|
|
4
|
+
import { Message, MessageAvatar, MessageContent, MessageActionBar } 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 type { ChatMessage } from './chat-types';
|
|
9
|
+
|
|
10
|
+
interface Props extends Record<string, unknown> {
|
|
11
|
+
/** The full message object. Set as a JS property. */
|
|
12
|
+
message?: ChatMessage;
|
|
13
|
+
/** Convenience for simple cases when not passing a `message` object. */
|
|
14
|
+
role?: 'user' | 'assistant';
|
|
15
|
+
/** Convenience content (used when `message` is not set). */
|
|
16
|
+
content?: string;
|
|
17
|
+
/** Force markdown on/off. Defaults to on for assistant, off for user. */
|
|
18
|
+
markdown?: boolean;
|
|
19
|
+
/** Text/markdown sizing for the message body. */
|
|
20
|
+
proseSize?: ProseSize;
|
|
21
|
+
/** Shiki theme name used for fenced code blocks in the content. */
|
|
22
|
+
codeTheme?: string;
|
|
23
|
+
/** Disable syntax highlighting for code blocks (no Shiki loads). */
|
|
24
|
+
codeHighlight?: boolean;
|
|
25
|
+
/** Whether the action bar is always visible (`'always'`, default) or only
|
|
26
|
+
* revealed on hover of the message row (`'hover'`). */
|
|
27
|
+
actionsReveal?: 'always' | 'hover';
|
|
28
|
+
/** Convenience avatar image URL (used when `message.avatar` is not set). */
|
|
29
|
+
avatarSrc?: string;
|
|
30
|
+
/** Convenience avatar fallback text (used when `message.avatar` is not set). */
|
|
31
|
+
avatarFallback?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Events fired by `<kai-message>`. */
|
|
35
|
+
interface Events {
|
|
36
|
+
/** An action button was clicked. `action` is the built-in name or custom id. */
|
|
37
|
+
'kai-message-action': { messageId: string; action: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* `<kai-message>` — a single message row: markdown/plain content, reasoning,
|
|
42
|
+
* tool calls, attachments, and action buttons, rendered from one `message`
|
|
43
|
+
* object (the same shape `<kai-chat>` uses per message). The keystone of the
|
|
44
|
+
* "compose your own message list" pattern. Emits `kai-message-action`.
|
|
45
|
+
*/
|
|
46
|
+
defineWebComponent<Props, Events>('kai-message', {
|
|
47
|
+
message: undefined,
|
|
48
|
+
role: 'assistant',
|
|
49
|
+
content: undefined,
|
|
50
|
+
markdown: undefined,
|
|
51
|
+
proseSize: 'sm',
|
|
52
|
+
codeTheme: 'github-dark-dimmed',
|
|
53
|
+
codeHighlight: true,
|
|
54
|
+
actionsReveal: 'always',
|
|
55
|
+
avatarSrc: undefined,
|
|
56
|
+
avatarFallback: undefined,
|
|
57
|
+
}, (props, { dispatch, flag, element }) => {
|
|
58
|
+
const outer = useChatConfig();
|
|
59
|
+
const msg = (): ChatMessage =>
|
|
60
|
+
props.message ?? { id: 'message', role: props.role ?? 'assistant', content: props.content ?? '' };
|
|
61
|
+
|
|
62
|
+
// Read declarative <kai-action> children from light DOM.
|
|
63
|
+
// Shadow DOM with no <slot> suppresses them visually — they're invisible data carriers.
|
|
64
|
+
const [slottedActions, setSlottedActions] = createSignal<import('../elements/chat-types').CustomAction[]>([]);
|
|
65
|
+
onMount(() => {
|
|
66
|
+
const read = () => {
|
|
67
|
+
const nodes = [...element.querySelectorAll('kai-action')];
|
|
68
|
+
setSlottedActions(nodes.map(n => ({
|
|
69
|
+
id: n.id || n.getAttribute('action') || '',
|
|
70
|
+
label: n.textContent?.trim() || n.getAttribute('label') || n.id || '',
|
|
71
|
+
icon: n.getAttribute('icon') ?? undefined,
|
|
72
|
+
tooltip: n.getAttribute('tooltip') ?? undefined,
|
|
73
|
+
})));
|
|
74
|
+
};
|
|
75
|
+
read();
|
|
76
|
+
const observer = new MutationObserver(read);
|
|
77
|
+
observer.observe(element, { childList: true, attributes: true, subtree: true });
|
|
78
|
+
onCleanup(() => observer.disconnect());
|
|
79
|
+
});
|
|
80
|
+
const isUser = () => msg().role === 'user';
|
|
81
|
+
const avatar = () =>
|
|
82
|
+
msg().avatar ??
|
|
83
|
+
(props.avatarSrc || props.avatarFallback
|
|
84
|
+
? { src: props.avatarSrc as string | undefined, fallback: props.avatarFallback as string | undefined }
|
|
85
|
+
: undefined);
|
|
86
|
+
const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always');
|
|
87
|
+
// markdown: explicit prop/attribute wins; otherwise default by role.
|
|
88
|
+
const markdownExplicit = () =>
|
|
89
|
+
element.hasAttribute('markdown') || props.markdown === true || props.markdown === false;
|
|
90
|
+
const useMarkdown = () => (markdownExplicit() ? flag('markdown') : !isUser());
|
|
91
|
+
|
|
92
|
+
const body = () => (
|
|
93
|
+
<>
|
|
94
|
+
<Show when={msg().reasoning}>
|
|
95
|
+
<Reasoning class="mb-2 w-full">
|
|
96
|
+
<ReasoningTrigger>{msg().reasoning!.label ?? 'Reasoning'}</ReasoningTrigger>
|
|
97
|
+
<ReasoningContent markdown>{msg().reasoning!.text}</ReasoningContent>
|
|
98
|
+
</Reasoning>
|
|
99
|
+
</Show>
|
|
100
|
+
<For each={msg().tools ?? []}>
|
|
101
|
+
{(tp) => <Tool toolPart={tp} class="mb-2 w-full" />}
|
|
102
|
+
</For>
|
|
103
|
+
<Show when={msg().attachments?.length}>
|
|
104
|
+
<Attachments variant="inline" class={isUser() ? 'mb-2 justify-end' : 'mb-2'}>
|
|
105
|
+
<For each={msg().attachments!}>
|
|
106
|
+
{(att) => (
|
|
107
|
+
<Attachment data={att}>
|
|
108
|
+
<AttachmentPreview />
|
|
109
|
+
<AttachmentInfo />
|
|
110
|
+
</Attachment>
|
|
111
|
+
)}
|
|
112
|
+
</For>
|
|
113
|
+
</Attachments>
|
|
114
|
+
</Show>
|
|
115
|
+
<MessageContent
|
|
116
|
+
markdown={useMarkdown()}
|
|
117
|
+
class={isUser()
|
|
118
|
+
? 'bg-muted text-primary max-w-[85%] rounded-2xl px-4 py-2'
|
|
119
|
+
: 'bg-transparent p-0'}
|
|
120
|
+
>
|
|
121
|
+
{msg().content}
|
|
122
|
+
</MessageContent>
|
|
123
|
+
<Show when={(msg().actions?.length ?? 0) > 0 || slottedActions().length > 0}>
|
|
124
|
+
<MessageActionBar
|
|
125
|
+
actions={[...(msg().actions ?? []), ...slottedActions()]}
|
|
126
|
+
reveal={reveal()}
|
|
127
|
+
onAction={(action) => dispatch('kai-message-action', { messageId: msg().id, action })}
|
|
128
|
+
/>
|
|
129
|
+
</Show>
|
|
130
|
+
</>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Row carries `group` so a hover-revealed action bar fades in on row hover.
|
|
134
|
+
const rowGroup = () => (reveal() === 'hover' ? 'group ' : '');
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<ChatConfig
|
|
138
|
+
proseSize={props.proseSize}
|
|
139
|
+
codeTheme={props.codeTheme}
|
|
140
|
+
codeHighlight={flag('codeHighlight')}
|
|
141
|
+
portalMount={outer.portalMount()}
|
|
142
|
+
>
|
|
143
|
+
<Show
|
|
144
|
+
when={avatar()}
|
|
145
|
+
fallback={
|
|
146
|
+
<Message class={`${rowGroup()}${isUser() ? 'flex-col items-end' : 'flex-col items-start'}`}>
|
|
147
|
+
{body()}
|
|
148
|
+
</Message>
|
|
149
|
+
}
|
|
150
|
+
>
|
|
151
|
+
{(av) => (
|
|
152
|
+
<Message class={rowGroup()}>
|
|
153
|
+
<MessageAvatar src={av().src ?? ''} alt={av().alt ?? ''} fallback={av().fallback} />
|
|
154
|
+
<div class={`flex min-w-0 flex-1 flex-col ${isUser() ? 'items-end' : 'items-start'}`}>
|
|
155
|
+
{body()}
|
|
156
|
+
</div>
|
|
157
|
+
</Message>
|
|
158
|
+
)}
|
|
159
|
+
</Show>
|
|
160
|
+
</ChatConfig>
|
|
161
|
+
);
|
|
162
|
+
});
|