@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,185 @@
|
|
|
1
|
+
import { createEffect, createSignal, onMount, onCleanup } from 'solid-js';
|
|
2
|
+
import { defineWebComponent } from './define';
|
|
3
|
+
import { DefaultPromptInput } from './default-input';
|
|
4
|
+
import type { AttachmentData } from '../components/attachments';
|
|
5
|
+
import type { SlashCommandItem } from '../components/slash-command';
|
|
6
|
+
import type { CustomAction } from './chat-types';
|
|
7
|
+
|
|
8
|
+
/** Parse a single light-DOM `<kai-slash-command>` element into a SlashCommandItem.
|
|
9
|
+
* Attribute mapping:
|
|
10
|
+
* - `command` → SlashCommandItem.id (required; empty string fallback)
|
|
11
|
+
* - textContent → SlashCommandItem.label (primary); `label` attr as fallback
|
|
12
|
+
* - `description` → SlashCommandItem.description
|
|
13
|
+
* - `category` → SlashCommandItem.category
|
|
14
|
+
*/
|
|
15
|
+
export function parseKaiSlashCommandElement(n: Element): SlashCommandItem {
|
|
16
|
+
return {
|
|
17
|
+
id: n.getAttribute('command') ?? '',
|
|
18
|
+
label: n.textContent?.trim() || n.getAttribute('label') || n.getAttribute('command') || '',
|
|
19
|
+
description: n.getAttribute('description') ?? undefined,
|
|
20
|
+
category: n.getAttribute('category') ?? undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Props extends Record<string, unknown> {
|
|
25
|
+
/** Controlled value of the input. When set, the host owns the text and must
|
|
26
|
+
* update it on `kai-value-change`; leave unset for uncontrolled behavior. */
|
|
27
|
+
value?: string;
|
|
28
|
+
/** Placeholder text shown in the empty input. */
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
/** Disable the input and submit button entirely (non-interactive). */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Show the loading/streaming state and block submit (use while awaiting a
|
|
33
|
+
* reply). */
|
|
34
|
+
loading?: boolean;
|
|
35
|
+
/** Starter prompts shown above the input. Clicking one follows
|
|
36
|
+
* `suggestionMode`. Set as a JS property. */
|
|
37
|
+
suggestions?: string[];
|
|
38
|
+
/** What clicking a suggestion does: `'submit'` (default) sends it immediately
|
|
39
|
+
* as if typed and submitted; `'fill'` just places it in the input. */
|
|
40
|
+
suggestionMode?: 'submit' | 'fill';
|
|
41
|
+
/** Slash commands — when set, typing `/` opens the command palette. Set as a
|
|
42
|
+
* JS property. */
|
|
43
|
+
slashCommands?: SlashCommandItem[];
|
|
44
|
+
/** Command ids to highlight as active. */
|
|
45
|
+
slashActiveIds?: string[];
|
|
46
|
+
/** Single-line palette rows. */
|
|
47
|
+
slashCompact?: boolean;
|
|
48
|
+
/** Show a Search (Globe) button in the left toolbar; clicking it fires a
|
|
49
|
+
* `search` event. */
|
|
50
|
+
search?: boolean;
|
|
51
|
+
/** Show a Voice (Mic) button in the left toolbar; clicking it fires a `voice`
|
|
52
|
+
* event. */
|
|
53
|
+
voice?: boolean;
|
|
54
|
+
/** When set and `loading` is true, the send button is replaced by a Stop
|
|
55
|
+
* button (square icon, "Stop" aria-label). Clicking it fires `kai-stop`. */
|
|
56
|
+
stoppable?: boolean;
|
|
57
|
+
/** Attachments to seed the input with (so a consumer can pre-populate staged
|
|
58
|
+
* files without an upload). Set as a JS property; the element then manages its
|
|
59
|
+
* own attachment state from there (add via the paperclip, remove per chip). */
|
|
60
|
+
attachments?: AttachmentData[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Events fired by `<kai-prompt-input>`. */
|
|
64
|
+
interface Events {
|
|
65
|
+
/** The user submitted the prompt (Enter or send button) with its attachments. */
|
|
66
|
+
'kai-submit': { value: string; attachments: AttachmentData[] };
|
|
67
|
+
/** The input text changed (fires on every keystroke). */
|
|
68
|
+
'kai-value-change': { value: string };
|
|
69
|
+
/** A suggestion was clicked while `suggestion-mode="fill"`. */
|
|
70
|
+
'kai-suggestion-click': { value: string };
|
|
71
|
+
/** A slash command was chosen from the palette. */
|
|
72
|
+
'kai-slash-select': { command: SlashCommandItem };
|
|
73
|
+
/** The Search (Globe) toolbar button was clicked. */
|
|
74
|
+
'kai-search': Record<string, never>;
|
|
75
|
+
/** The Voice (Mic) toolbar button was clicked. */
|
|
76
|
+
'kai-voice': Record<string, never>;
|
|
77
|
+
/** The Stop button was clicked while `stoppable` and `loading` are both true. */
|
|
78
|
+
'kai-stop': Record<string, never>;
|
|
79
|
+
/** A custom `<kai-action>` toolbar button was clicked. `action` is the `id` of
|
|
80
|
+
* the `<kai-action>` element that was clicked. */
|
|
81
|
+
'kai-toolbar-action': { action: string };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
defineWebComponent<Props, Events>('kai-prompt-input', {
|
|
85
|
+
value: undefined,
|
|
86
|
+
placeholder: 'Send a message...',
|
|
87
|
+
disabled: false,
|
|
88
|
+
loading: false,
|
|
89
|
+
suggestions: undefined,
|
|
90
|
+
suggestionMode: 'submit',
|
|
91
|
+
slashCommands: undefined,
|
|
92
|
+
slashActiveIds: undefined,
|
|
93
|
+
slashCompact: false,
|
|
94
|
+
search: false,
|
|
95
|
+
voice: false,
|
|
96
|
+
stoppable: false,
|
|
97
|
+
attachments: undefined,
|
|
98
|
+
}, (props, { dispatch, flag, element }) => {
|
|
99
|
+
const [internal, setInternal] = createSignal(props.value ?? '');
|
|
100
|
+
// Seed staged attachments from the `attachments` property; the element manages
|
|
101
|
+
// its own state from there (paperclip adds, per-chip remove deletes).
|
|
102
|
+
const [attachments, setAttachments] = createSignal<AttachmentData[]>(props.attachments ?? []);
|
|
103
|
+
// Re-seed when the `attachments` property is (re)assigned by the consumer
|
|
104
|
+
// (e.g. set via a `ref` after mount). Subsequent in-element edits stay local.
|
|
105
|
+
createEffect(() => {
|
|
106
|
+
if (props.attachments) setAttachments(props.attachments);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Read declarative <kai-action> and <kai-slash-command> children from light DOM —
|
|
110
|
+
// same pattern as kai-message. Shadow DOM with no <slot> suppresses them visually;
|
|
111
|
+
// they are invisible data carriers. One MutationObserver covers both element types.
|
|
112
|
+
const [toolbarActions, setToolbarActions] = createSignal<CustomAction[]>([]);
|
|
113
|
+
const [slottedSlashCommands, setSlottedSlashCommands] = createSignal<SlashCommandItem[]>([]);
|
|
114
|
+
onMount(() => {
|
|
115
|
+
const readActions = () => {
|
|
116
|
+
const nodes = [...element.querySelectorAll('kai-action')];
|
|
117
|
+
setToolbarActions(nodes.map(n => ({
|
|
118
|
+
id: n.id || n.getAttribute('action') || '',
|
|
119
|
+
label: n.textContent?.trim() || n.getAttribute('label') || n.id || '',
|
|
120
|
+
icon: n.getAttribute('icon') ?? undefined,
|
|
121
|
+
tooltip: n.getAttribute('tooltip') ?? undefined,
|
|
122
|
+
})));
|
|
123
|
+
};
|
|
124
|
+
const readSlashCommands = () => {
|
|
125
|
+
const nodes = [...element.querySelectorAll('kai-slash-command')];
|
|
126
|
+
setSlottedSlashCommands(nodes.map(parseKaiSlashCommandElement));
|
|
127
|
+
};
|
|
128
|
+
const readAll = () => { readActions(); readSlashCommands(); };
|
|
129
|
+
readAll();
|
|
130
|
+
const observer = new MutationObserver(readAll);
|
|
131
|
+
observer.observe(element, { childList: true, attributes: true, subtree: true });
|
|
132
|
+
onCleanup(() => observer.disconnect());
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const current = () => props.value ?? internal();
|
|
136
|
+
|
|
137
|
+
const handleChange = (v: string) => { setInternal(v); dispatch('kai-value-change', { value: v }); };
|
|
138
|
+
const handleSubmit = () => {
|
|
139
|
+
dispatch('kai-submit', { value: current(), attachments: attachments() });
|
|
140
|
+
setAttachments([]);
|
|
141
|
+
};
|
|
142
|
+
const handleSuggestionClick = (v: string) => {
|
|
143
|
+
if ((props.suggestionMode ?? 'submit') === 'fill') {
|
|
144
|
+
handleChange(v);
|
|
145
|
+
dispatch('kai-suggestion-click', { value: v });
|
|
146
|
+
} else {
|
|
147
|
+
// Default: behave as if the user typed the suggestion and pressed submit.
|
|
148
|
+
dispatch('kai-submit', { value: v, attachments: attachments() });
|
|
149
|
+
setAttachments([]);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Prop slash commands take precedence; slotted children are appended after.
|
|
154
|
+
const allSlashCommands = () => [
|
|
155
|
+
...(props.slashCommands ?? []),
|
|
156
|
+
...slottedSlashCommands(),
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<DefaultPromptInput
|
|
161
|
+
value={current()}
|
|
162
|
+
placeholder={props.placeholder}
|
|
163
|
+
disabled={flag('disabled')}
|
|
164
|
+
loading={flag('loading')}
|
|
165
|
+
stoppable={flag('stoppable')}
|
|
166
|
+
suggestions={props.suggestions}
|
|
167
|
+
attachments={attachments()}
|
|
168
|
+
slashCommands={allSlashCommands().length ? allSlashCommands() : undefined}
|
|
169
|
+
slashActiveIds={props.slashActiveIds}
|
|
170
|
+
slashCompact={flag('slashCompact')}
|
|
171
|
+
search={flag('search')}
|
|
172
|
+
voice={flag('voice')}
|
|
173
|
+
toolbarActions={toolbarActions()}
|
|
174
|
+
onValueChange={handleChange}
|
|
175
|
+
onSubmit={handleSubmit}
|
|
176
|
+
onSuggestionClick={handleSuggestionClick}
|
|
177
|
+
onAttachmentsChange={setAttachments}
|
|
178
|
+
onSearch={() => dispatch('kai-search')}
|
|
179
|
+
onVoice={() => dispatch('kai-voice')}
|
|
180
|
+
onStop={() => dispatch('kai-stop')}
|
|
181
|
+
onSlashSelect={(command) => dispatch('kai-slash-select', { command })}
|
|
182
|
+
onAction={(id) => dispatch('kai-toolbar-action', { action: id })}
|
|
183
|
+
/>
|
|
184
|
+
);
|
|
185
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the declarative `<kai-suggestion>` light-DOM API of
|
|
3
|
+
* `<kai-suggestions>`.
|
|
4
|
+
*
|
|
5
|
+
* Strategy: the `defineWebComponent` call registers a real Shadow-DOM custom
|
|
6
|
+
* element which requires a full browser environment (Constructable Stylesheets,
|
|
7
|
+
* shadow roots, etc.) and is therefore not suitable for jsdom unit tests.
|
|
8
|
+
* Instead we:
|
|
9
|
+
* 1. Test the exported `parseSuggestionNode` helper in isolation — it has no
|
|
10
|
+
* DOM dependencies beyond `Element`, which jsdom provides perfectly.
|
|
11
|
+
* 2. Test that the merged list of prop + slotted items renders chip buttons
|
|
12
|
+
* and fires click handlers correctly via `PromptSuggestion` directly,
|
|
13
|
+
* mirroring the pattern used in `message-action-bar.test.tsx`.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
16
|
+
import '@testing-library/jest-dom/vitest';
|
|
17
|
+
import { render, cleanup, fireEvent } from '@solidjs/testing-library';
|
|
18
|
+
import { For } from 'solid-js';
|
|
19
|
+
import { parseSuggestionNode } from './prompt-suggestions';
|
|
20
|
+
import { PromptSuggestion } from '../components/prompt-suggestion';
|
|
21
|
+
|
|
22
|
+
afterEach(cleanup);
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// parseSuggestionNode — pure helper
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
describe('parseSuggestionNode', () => {
|
|
29
|
+
function makeNode(textContent: string, value?: string): Element {
|
|
30
|
+
const el = document.createElement('kai-suggestion');
|
|
31
|
+
el.textContent = textContent;
|
|
32
|
+
if (value !== undefined) el.setAttribute('value', value);
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('uses textContent as label', () => {
|
|
37
|
+
const item = parseSuggestionNode(makeNode('Use Vue'));
|
|
38
|
+
expect(item).toMatchObject({ label: 'Use Vue' });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('uses value attribute as value when present', () => {
|
|
42
|
+
const item = parseSuggestionNode(makeNode('Use Vue', 'vue'));
|
|
43
|
+
expect(item).toMatchObject({ label: 'Use Vue', value: 'vue' });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('falls back to textContent as value when value attribute is absent', () => {
|
|
47
|
+
const item = parseSuggestionNode(makeNode('Use Vue'));
|
|
48
|
+
expect(item).toMatchObject({ label: 'Use Vue', value: 'Use Vue' });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('trims leading/trailing whitespace from textContent', () => {
|
|
52
|
+
const item = parseSuggestionNode(makeNode(' Explain the architecture '));
|
|
53
|
+
expect(item).toMatchObject({ label: 'Explain the architecture' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles empty textContent with a value attribute', () => {
|
|
57
|
+
const item = parseSuggestionNode(makeNode('', 'vue'));
|
|
58
|
+
expect(item).toMatchObject({ label: '', value: 'vue' });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Merge + render: prop suggestions + slotted suggestions combine correctly
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
type Item = string | { label: string; value?: string };
|
|
67
|
+
|
|
68
|
+
const labelOf = (s: Item) => (typeof s === 'string' ? s : s.label);
|
|
69
|
+
const valueOf = (s: Item) => (typeof s === 'string' ? s : s.value ?? s.label);
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Minimal test harness that renders a list of `Item`s as `PromptSuggestion`
|
|
73
|
+
* chips and records which value was clicked — mirrors the render path in
|
|
74
|
+
* `prompt-suggestions.tsx`.
|
|
75
|
+
*/
|
|
76
|
+
function SuggestionList(props: { items: Item[]; onSelect: (value: string) => void }) {
|
|
77
|
+
return (
|
|
78
|
+
<div>
|
|
79
|
+
<For each={props.items}>
|
|
80
|
+
{(s) => (
|
|
81
|
+
<PromptSuggestion onClick={() => props.onSelect(valueOf(s))}>
|
|
82
|
+
{labelOf(s)}
|
|
83
|
+
</PromptSuggestion>
|
|
84
|
+
)}
|
|
85
|
+
</For>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('suggestion chip rendering and click events', () => {
|
|
91
|
+
it('renders one chip per item', () => {
|
|
92
|
+
const { getByText } = render(() => (
|
|
93
|
+
<SuggestionList
|
|
94
|
+
items={['Explain the architecture', 'Show me a code example']}
|
|
95
|
+
onSelect={() => {}}
|
|
96
|
+
/>
|
|
97
|
+
));
|
|
98
|
+
expect(getByText('Explain the architecture')).toBeInTheDocument();
|
|
99
|
+
expect(getByText('Show me a code example')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('fires onSelect with the string value when a string item is clicked', () => {
|
|
103
|
+
const onSelect = vi.fn();
|
|
104
|
+
const { getByText } = render(() => (
|
|
105
|
+
<SuggestionList items={['Use Vue']} onSelect={onSelect} />
|
|
106
|
+
));
|
|
107
|
+
fireEvent.click(getByText('Use Vue'));
|
|
108
|
+
expect(onSelect).toHaveBeenCalledWith('Use Vue');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('fires onSelect with item.value for an object item with an explicit value', () => {
|
|
112
|
+
const onSelect = vi.fn();
|
|
113
|
+
const { getByText } = render(() => (
|
|
114
|
+
<SuggestionList
|
|
115
|
+
items={[{ label: 'Use Vue', value: 'vue' }]}
|
|
116
|
+
onSelect={onSelect}
|
|
117
|
+
/>
|
|
118
|
+
));
|
|
119
|
+
fireEvent.click(getByText('Use Vue'));
|
|
120
|
+
expect(onSelect).toHaveBeenCalledWith('vue');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('falls back to label as value for an object item without explicit value', () => {
|
|
124
|
+
const onSelect = vi.fn();
|
|
125
|
+
const { getByText } = render(() => (
|
|
126
|
+
<SuggestionList
|
|
127
|
+
items={[{ label: 'Use Vue' }]}
|
|
128
|
+
onSelect={onSelect}
|
|
129
|
+
/>
|
|
130
|
+
));
|
|
131
|
+
fireEvent.click(getByText('Use Vue'));
|
|
132
|
+
expect(onSelect).toHaveBeenCalledWith('Use Vue');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('renders prop items before declarative (slotted) items', () => {
|
|
136
|
+
// Simulate the merge: prop first, slotted second.
|
|
137
|
+
const propItems: Item[] = [{ label: 'Prop item', value: 'prop' }];
|
|
138
|
+
const slottedItems: Item[] = [{ label: 'Slotted item', value: 'slotted' }];
|
|
139
|
+
const merged = [...propItems, ...slottedItems];
|
|
140
|
+
|
|
141
|
+
const { getAllByRole } = render(() => (
|
|
142
|
+
<SuggestionList items={merged} onSelect={() => {}} />
|
|
143
|
+
));
|
|
144
|
+
const buttons = getAllByRole('button');
|
|
145
|
+
expect(buttons).toHaveLength(2);
|
|
146
|
+
expect(buttons[0]).toHaveTextContent('Prop item');
|
|
147
|
+
expect(buttons[1]).toHaveTextContent('Slotted item');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('parseSuggestionNode produces items that render and fire the correct value', () => {
|
|
151
|
+
// End-to-end: parse a DOM node → render it → click → verify event value.
|
|
152
|
+
const onSelect = vi.fn();
|
|
153
|
+
const el = document.createElement('kai-suggestion');
|
|
154
|
+
el.textContent = 'Use Vue';
|
|
155
|
+
el.setAttribute('value', 'vue');
|
|
156
|
+
|
|
157
|
+
const parsed = parseSuggestionNode(el);
|
|
158
|
+
const { getByText } = render(() => (
|
|
159
|
+
<SuggestionList items={[parsed]} onSelect={onSelect} />
|
|
160
|
+
));
|
|
161
|
+
fireEvent.click(getByText('Use Vue'));
|
|
162
|
+
expect(onSelect).toHaveBeenCalledWith('vue');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
type Item = string | { label: string; value?: string };
|
|
7
|
+
|
|
8
|
+
// The web components are custom DOM elements, so declare the tags for JSX.
|
|
9
|
+
declare module 'solid-js' {
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
11
|
+
namespace JSX {
|
|
12
|
+
interface IntrinsicElements {
|
|
13
|
+
'kai-suggestions': JSX.HTMLAttributes<HTMLElement> & {
|
|
14
|
+
variant?: string;
|
|
15
|
+
size?: string;
|
|
16
|
+
block?: boolean | string;
|
|
17
|
+
highlight?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Light-DOM data carrier for declarative suggestion chips inside `<kai-suggestions>`. */
|
|
20
|
+
'kai-suggestion': JSX.HTMLAttributes<HTMLElement> & {
|
|
21
|
+
value?: string;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const suggestions: Item[] = [
|
|
28
|
+
'Explain the architecture',
|
|
29
|
+
'Show me a code example',
|
|
30
|
+
"What's deferred?",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Render `<kai-suggestions>` with `suggestions` set as a property. */
|
|
34
|
+
function SuggestionsElement(props: { suggestions: Item[]; variant?: string; size?: string; block?: boolean; highlight?: string }) {
|
|
35
|
+
let el: (HTMLElement & { suggestions?: Item[] }) | undefined;
|
|
36
|
+
onMount(() => {
|
|
37
|
+
if (!el) return;
|
|
38
|
+
el.suggestions = props.suggestions;
|
|
39
|
+
el.addEventListener('kai-select', (e) => {
|
|
40
|
+
const ev = e as CustomEvent<{ value: string }>;
|
|
41
|
+
console.log('kai-select', ev.detail.value);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
return (
|
|
45
|
+
<kai-suggestions
|
|
46
|
+
ref={(e) => (el = e as HTMLElement)}
|
|
47
|
+
variant={props.variant}
|
|
48
|
+
size={props.size}
|
|
49
|
+
block={props.block ? true : undefined}
|
|
50
|
+
highlight={props.highlight}
|
|
51
|
+
style={{ display: 'block', padding: '24px', 'max-width': '560px' }}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const HTML_SNIPPET = `<!-- Works in any framework or plain HTML -->
|
|
57
|
+
<kai-suggestions id="suggs" variant="outline"></kai-suggestions>
|
|
58
|
+
|
|
59
|
+
<script type="module">
|
|
60
|
+
import '@kitn.ai/ui/elements'; // registers the custom elements
|
|
61
|
+
|
|
62
|
+
const suggs = document.getElementById('suggs');
|
|
63
|
+
suggs.suggestions = ['Explain the architecture', 'Show me a code example'];
|
|
64
|
+
suggs.addEventListener('kai-select', (e) => console.log(e.detail.value));
|
|
65
|
+
</script>`;
|
|
66
|
+
|
|
67
|
+
const meta = {
|
|
68
|
+
title: 'Components/Suggestions',
|
|
69
|
+
tags: ['autodocs'],
|
|
70
|
+
argTypes: argTypesFor('kai-suggestions'),
|
|
71
|
+
parameters: {
|
|
72
|
+
layout: 'fullscreen',
|
|
73
|
+
docs: {
|
|
74
|
+
description: specDescription('kai-suggestions', [
|
|
75
|
+
'`<kai-suggestions>` is the framework-agnostic **web component** for a row (or list) of clickable suggestion chips — starter prompts or follow-ups — isolated in **Shadow DOM**.',
|
|
76
|
+
'**When to use:** offering the user quick prompts to click instead of type, usually above an input. In SolidJS, use the `PromptSuggestion` primitive.',
|
|
77
|
+
'**Placement:** above the prompt input in the empty/welcome state, or below the last assistant message as follow-up chips; it is a `block` element and takes the full width of its container.',
|
|
78
|
+
"**How to use:** register once with `import '@kitn.ai/ui/elements'`, set the `suggestions` **property** (strings, or `{ label, value }` when the displayed text differs from the emitted value), choose a `variant` and `size` (`sm` | `md` | `lg`; pills default to `lg`), optionally add the `block` flag for full-width rows or a `highlight` substring to emphasize, and listen for the `kai-select` **CustomEvent**.",
|
|
79
|
+
'See the **Code** tab for HTML usage.',
|
|
80
|
+
]),
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
} satisfies Meta;
|
|
84
|
+
|
|
85
|
+
export default meta;
|
|
86
|
+
type Story = StoryObj;
|
|
87
|
+
|
|
88
|
+
/** Default outline pills, wrapping in a row. */
|
|
89
|
+
export const Default: Story = {
|
|
90
|
+
render: () => <SuggestionsElement suggestions={suggestions} variant="outline" />,
|
|
91
|
+
parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Ghost variant. */
|
|
95
|
+
export const Ghost: Story = {
|
|
96
|
+
render: () => <SuggestionsElement suggestions={suggestions} variant="ghost" />,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Full-width left-aligned rows via the `block` flag. */
|
|
100
|
+
export const Block: Story = {
|
|
101
|
+
render: () => <SuggestionsElement suggestions={suggestions} variant="outline" block />,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const searchSuggestions: Item[] = [
|
|
105
|
+
'How does SolidJS handle reactivity?',
|
|
106
|
+
'What makes SolidJS fast?',
|
|
107
|
+
'SolidJS vs Svelte comparison',
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/** Filtered-search list: each row highlights the matched substring via the
|
|
111
|
+
* `highlight` attribute (which forces the list-row layout). */
|
|
112
|
+
export const WithHighlightedSearch: Story = {
|
|
113
|
+
name: 'With Highlighted Search',
|
|
114
|
+
render: () => (
|
|
115
|
+
<SuggestionsElement suggestions={searchSuggestions} highlight="Solid" />
|
|
116
|
+
),
|
|
117
|
+
parameters: {
|
|
118
|
+
docs: {
|
|
119
|
+
source: {
|
|
120
|
+
code: `<kai-suggestions id="suggs" highlight="Solid"></kai-suggestions>
|
|
121
|
+
|
|
122
|
+
<script type="module">
|
|
123
|
+
import '@kitn.ai/ui/elements';
|
|
124
|
+
const suggs = document.getElementById('suggs');
|
|
125
|
+
suggs.suggestions = [
|
|
126
|
+
'How does SolidJS handle reactivity?',
|
|
127
|
+
'What makes SolidJS fast?',
|
|
128
|
+
'SolidJS vs Svelte comparison',
|
|
129
|
+
];
|
|
130
|
+
</script>`,
|
|
131
|
+
language: 'html',
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Sizes side by side — the default pill (`lg`) vs the smaller `sm` pill. */
|
|
138
|
+
export const Sizes: Story = {
|
|
139
|
+
render: () => (
|
|
140
|
+
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '8px', padding: '24px' }}>
|
|
141
|
+
<div>
|
|
142
|
+
<div style={{ 'font-size': '12px', opacity: '0.6', margin: '0 0 4px' }}>Default (lg)</div>
|
|
143
|
+
<SuggestionsElement suggestions={suggestions} variant="outline" />
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<div style={{ 'font-size': '12px', opacity: '0.6', margin: '0 0 4px' }}>Small (sm)</div>
|
|
147
|
+
<SuggestionsElement suggestions={suggestions} variant="outline" size="sm" />
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
),
|
|
151
|
+
parameters: {
|
|
152
|
+
docs: {
|
|
153
|
+
source: {
|
|
154
|
+
code: `<!-- default pill -->
|
|
155
|
+
<kai-suggestions variant="outline"></kai-suggestions>
|
|
156
|
+
<!-- smaller pill -->
|
|
157
|
+
<kai-suggestions variant="outline" size="sm"></kai-suggestions>`,
|
|
158
|
+
language: 'html',
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const DECLARATIVE_HTML_SNIPPET = `<!-- Works in any framework or plain HTML — no JS property assignment needed -->
|
|
165
|
+
<kai-suggestions id="suggs" variant="outline">
|
|
166
|
+
<kai-suggestion value="explain">Explain the architecture</kai-suggestion>
|
|
167
|
+
<kai-suggestion value="example">Show me a code example</kai-suggestion>
|
|
168
|
+
<kai-suggestion value="deferred">What's deferred?</kai-suggestion>
|
|
169
|
+
</kai-suggestions>
|
|
170
|
+
|
|
171
|
+
<script type="module">
|
|
172
|
+
import '@kitn.ai/ui/elements'; // registers the custom elements
|
|
173
|
+
|
|
174
|
+
document.getElementById('suggs').addEventListener('kai-select', (e) => {
|
|
175
|
+
console.log('kai-select', e.detail.value);
|
|
176
|
+
});
|
|
177
|
+
</script>`;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Declare each suggestion as a `<kai-suggestion>` child element — no `suggestions`
|
|
181
|
+
* property or JS array wiring needed. The `value` attribute sets the emitted
|
|
182
|
+
* value; `textContent` is the displayed label. Children are light-DOM data
|
|
183
|
+
* carriers hidden by the Shadow DOM — pure data, no visible output of their own.
|
|
184
|
+
* Mix with the `suggestions` prop: prop items render first, declarative children after.
|
|
185
|
+
*/
|
|
186
|
+
export const DeclarativeSuggestions: Story = {
|
|
187
|
+
name: 'Declarative Suggestions (kai-suggestion)',
|
|
188
|
+
render: () => {
|
|
189
|
+
let el: HTMLElement | undefined;
|
|
190
|
+
onMount(() => {
|
|
191
|
+
if (!el) return;
|
|
192
|
+
el.addEventListener('kai-select', (e) =>
|
|
193
|
+
console.log('kai-select', (e as CustomEvent<{ value: string }>).detail.value),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
return (
|
|
197
|
+
<kai-suggestions
|
|
198
|
+
ref={(e) => (el = e as HTMLElement)}
|
|
199
|
+
variant="outline"
|
|
200
|
+
style={{ display: 'block', padding: '24px', 'max-width': '560px' }}
|
|
201
|
+
>
|
|
202
|
+
<kai-suggestion value="explain">Explain the architecture</kai-suggestion>
|
|
203
|
+
<kai-suggestion value="example">Show me a code example</kai-suggestion>
|
|
204
|
+
<kai-suggestion value="deferred">What's deferred?</kai-suggestion>
|
|
205
|
+
</kai-suggestions>
|
|
206
|
+
);
|
|
207
|
+
},
|
|
208
|
+
parameters: {
|
|
209
|
+
docs: {
|
|
210
|
+
source: { code: DECLARATIVE_HTML_SNIPPET, language: 'html' },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { For, createSignal, onMount, onCleanup } from 'solid-js';
|
|
2
|
+
import { defineWebComponent } from './define';
|
|
3
|
+
import { PromptSuggestion } from '../components/prompt-suggestion';
|
|
4
|
+
|
|
5
|
+
type Item = string | { label: string; value?: string };
|
|
6
|
+
|
|
7
|
+
interface Props extends Record<string, unknown> {
|
|
8
|
+
/** The suggestions. Strings, or `{ label, value }` when the displayed text
|
|
9
|
+
* and the emitted value differ. Set as a JS property. */
|
|
10
|
+
suggestions: Item[];
|
|
11
|
+
/** Chip style: `'outline'` (default), `'ghost'`, or `'default'` (filled). */
|
|
12
|
+
variant?: 'outline' | 'ghost' | 'default';
|
|
13
|
+
/** Size preset for each chip. Defaults to the pill default (`'lg'`); pass
|
|
14
|
+
* `'sm'` for smaller pills (or `'md'`). */
|
|
15
|
+
size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm';
|
|
16
|
+
/** Full-width left-aligned rows instead of pills. */
|
|
17
|
+
block?: boolean;
|
|
18
|
+
/** Substring to highlight within each suggestion. */
|
|
19
|
+
highlight?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Events fired by `<kai-suggestions>`. */
|
|
23
|
+
interface Events {
|
|
24
|
+
/** A suggestion was clicked. */
|
|
25
|
+
'kai-select': { value: string };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const labelOf = (s: Item) => (typeof s === 'string' ? s : s.label);
|
|
29
|
+
const valueOf = (s: Item) => (typeof s === 'string' ? s : s.value ?? s.label);
|
|
30
|
+
|
|
31
|
+
/** Parse a single `<kai-suggestion>` node into an `Item` descriptor. */
|
|
32
|
+
export function parseSuggestionNode(n: Element): Item {
|
|
33
|
+
const text = n.textContent?.trim() ?? '';
|
|
34
|
+
const value = n.getAttribute('value') ?? text;
|
|
35
|
+
return { label: text, value };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `<kai-suggestions>` — a row/list of suggestion chips. Data via the
|
|
40
|
+
* `suggestions` property; `variant`/`block`/`highlight` attributes; emits
|
|
41
|
+
* `select`.
|
|
42
|
+
*
|
|
43
|
+
* Alternatively, declare chips as `<kai-suggestion>` child elements
|
|
44
|
+
* (light-DOM data carriers — hidden by the Shadow DOM):
|
|
45
|
+
*
|
|
46
|
+
* ```html
|
|
47
|
+
* <kai-suggestions>
|
|
48
|
+
* <kai-suggestion value="vue">Use Vue</kai-suggestion>
|
|
49
|
+
* <kai-suggestion value="react">Use React</kai-suggestion>
|
|
50
|
+
* </kai-suggestions>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
defineWebComponent<Props, Events>('kai-suggestions', {
|
|
54
|
+
suggestions: [],
|
|
55
|
+
variant: 'outline',
|
|
56
|
+
size: undefined,
|
|
57
|
+
block: false,
|
|
58
|
+
highlight: undefined,
|
|
59
|
+
}, (props, { dispatch, flag, element }) => {
|
|
60
|
+
// Read declarative <kai-suggestion> children from light DOM.
|
|
61
|
+
// Shadow DOM with no <slot> suppresses them visually — they're invisible data carriers.
|
|
62
|
+
const [slottedSuggestions, setSlottedSuggestions] = createSignal<Item[]>([]);
|
|
63
|
+
onMount(() => {
|
|
64
|
+
const read = () => {
|
|
65
|
+
const nodes = [...element.querySelectorAll('kai-suggestion')];
|
|
66
|
+
setSlottedSuggestions(nodes.map(parseSuggestionNode));
|
|
67
|
+
};
|
|
68
|
+
read();
|
|
69
|
+
const observer = new MutationObserver(read);
|
|
70
|
+
observer.observe(element, { childList: true, attributes: true, subtree: true });
|
|
71
|
+
onCleanup(() => observer.disconnect());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Merge prop suggestions (first) with declarative children (after).
|
|
75
|
+
const allSuggestions = () => [...(props.suggestions ?? []), ...slottedSuggestions()];
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div class={flag('block') ? 'flex flex-col gap-2' : 'flex flex-wrap gap-2'}>
|
|
79
|
+
<For each={allSuggestions()}>
|
|
80
|
+
{(s) => (
|
|
81
|
+
<PromptSuggestion
|
|
82
|
+
variant={props.variant}
|
|
83
|
+
size={props.size}
|
|
84
|
+
block={flag('block')}
|
|
85
|
+
highlight={props.highlight}
|
|
86
|
+
onClick={() => dispatch('kai-select', { value: valueOf(s) })}
|
|
87
|
+
>
|
|
88
|
+
{labelOf(s)}
|
|
89
|
+
</PromptSuggestion>
|
|
90
|
+
)}
|
|
91
|
+
</For>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
});
|