import type { Meta, StoryObj } from 'storybook-solidjs-vite'; import { onMount } from 'solid-js'; import './register'; // side effect: registers , , import type { AttachmentData } from '../components/attachments'; import { argTypesFor, specDescription } from '../stories/docs/element-controls'; // The web components are custom DOM elements, so declare the tags for JSX. declare module 'solid-js' { // eslint-disable-next-line @typescript-eslint/no-namespace namespace JSX { interface IntrinsicElements { 'kc-prompt-input': JSX.HTMLAttributes; 'kc-action': JSX.HTMLAttributes & { icon?: string; tooltip?: string }; 'kc-slash-command': JSX.HTMLAttributes & { command?: string; description?: string; category?: string }; } } } const sampleSuggestions: string[] = [ 'Summarize this thread', 'Draft a reply', 'Explain like I am five', ]; function imgData(fill: string, glyph: string) { const svg = `${glyph}`; return 'data:image/svg+xml;utf8,' + encodeURIComponent(svg); } const sampleAttachments: AttachmentData[] = [ { id: 'a1', type: 'file', filename: 'architecture.png', mediaType: 'image/png', url: imgData('#7c3aed', '◆') }, { id: 'a2', type: 'file', filename: 'spec.pdf', mediaType: 'application/pdf' }, ]; interface PromptInputEl extends HTMLElement { value?: string; placeholder?: string; disabled?: boolean; loading?: boolean; suggestions?: string[]; search?: boolean; voice?: boolean; attachments?: AttachmentData[]; } /** Live demo of the actual `` custom element (Shadow DOM and all). */ function PromptInputElement(props: { search?: boolean; voice?: boolean; attachments?: AttachmentData[]; args?: Record }) { let el: PromptInputEl | undefined; onMount(() => { if (!el) return; // Default fixed data el.placeholder = 'Ask anything...'; el.suggestions = sampleSuggestions; if (props.search) el.setAttribute('search', ''); if (props.voice) el.setAttribute('voice', ''); if (props.attachments) el.attachments = props.attachments; // Scalar args from Controls const args = props.args; if (args) { const scalarNames = [ 'value', 'placeholder', 'disabled', 'loading', 'suggestionMode', 'slashCompact', 'search', 'voice', ]; for (const name of scalarNames) { if (name in args) (el as unknown as Record)[name] = args[name]; } } el.addEventListener('kc-search', () => console.log('search clicked')); el.addEventListener('kc-voice', () => console.log('voice clicked')); }); return ( (el = e as PromptInputEl)} style={{ display: 'block', width: '100%', padding: '16px' }} /> ); } const HTML_SNIPPET = ` `; const SOLID_SNIPPET = `import '@kitn.ai/chat/elements'; // registers the custom elements import { onMount } from 'solid-js'; function Composer() { let el: HTMLElement & { value?: string; placeholder?: string; disabled?: boolean; loading?: boolean; suggestions?: string[]; }; onMount(() => { el.placeholder = 'Ask anything...'; el.suggestions = ['Summarize this thread', 'Draft a reply']; }); return ( console.log('send:', e.detail.value)} on:kc-value-change={(e) => console.log('typing:', e.detail.value)} on:kc-suggestion-click={(e) => console.log('picked:', e.detail.value)} /> ); }`; const meta = { title: 'Components/PromptInput', tags: ['autodocs'], argTypes: argTypesFor('kc-prompt-input'), parameters: { layout: 'fullscreen', docs: { description: specDescription('kc-prompt-input', [ '`` is the framework-agnostic **web component** version of the chat composer — an auto-resizing textarea with a send button and optional suggestion chips, isolated in **Shadow DOM** so the host page\'s CSS can\'t leak in and the kit\'s styles can\'t leak out. SolidJS is bundled in, so the host needs nothing.', '**When to use:** adding a message composer to a non-Solid app (React, Vue, Svelte, plain HTML), or anywhere you want zero style conflicts. If you *are* in SolidJS and want fine-grained control, compose the `PromptInput` primitives instead.', '**How to use:** register once with `import \'@kitn.ai/chat/elements\'`, configure it with JS **properties** (`placeholder`, `value`, `disabled`, `loading`, `suggestions`, `attachments`) and flag attributes (`search`, `voice` to show the Globe/Mic toolbar buttons), and listen for **CustomEvents** (`kc-submit`, `kc-value-change`, `kc-suggestion-click`, `kc-search`, `kc-voice`) directly on the element. Leave `value` unset to let the element manage its own input state; seed `attachments` to pre-populate staged files. **Custom toolbar buttons:** place `` elements as children — they are invisible data carriers (Shadow DOM hides them) that the element reads and renders as extra ghost icon buttons in the left toolbar. Each click fires a `kc-toolbar-action` CustomEvent with `detail.action` equal to the action id (the same `` descriptor element that `` uses — composition symmetry).', '**Slash commands (declarative):** place `Label` elements as children — invisible data carriers merged with the `slashCommands` JS property. Typing `/` opens the palette with the combined list; selecting an item fires `kc-slash-select` with `detail.command`. Prop items appear first; declarative children are appended.', '**Placement:** pinned to the bottom of a chat surface, full width. Set `loading` while a response streams to show the busy state, and `disabled` to block input entirely.', 'See the **Code** tab below for the HTML usage; the *SolidJS* story shows the same element inside a Solid component.', ]), }, }, } satisfies Meta; export default meta; type Story = StoryObj; /** The element used the plain-HTML / any-framework way. */ export const Default: Story = { args: { placeholder: 'Send a message...', disabled: false, loading: false, suggestionMode: 'submit', slashCompact: false, search: false, voice: false, }, render: (args: Record) => , parameters: { docs: { source: { code: HTML_SNIPPET, language: 'html' } } }, }; /** The same element used inside a SolidJS component (properties via `ref`, events via `on:`). */ export const InSolidJS: Story = { name: 'In SolidJS', render: () => , parameters: { docs: { source: { code: SOLID_SNIPPET, language: 'tsx' } } }, }; const TOOLBAR_SNIPPET = ` `; /** With the **microphone** (and search) toolbar buttons enabled via the `voice` * and `search` flags. Clicking them fires `kc-voice` / `kc-search` CustomEvents. */ export const WithVoiceAndSearch: Story = { name: 'With Voice & Search', render: () => , parameters: { docs: { source: { code: TOOLBAR_SNIPPET, language: 'html' } } }, }; const ATTACHMENTS_SNIPPET = ` `; /** Pre-populated with a couple of **attachments** (an image + a file) via the * `attachments` property, with the mic shown too. The paperclip still adds * more, and each chip can be removed. */ export const WithAttachments: Story = { name: 'With Attachments', render: () => , parameters: { docs: { source: { code: ATTACHMENTS_SNIPPET, language: 'html' } } }, }; const CUSTOM_TOOLBAR_SNIPPET = ` `; const SLASH_COMMAND_SNIPPET = ` `; /** Composition: place **``** children inside `` to add * custom ghost icon buttons in the toolbar. Each click fires a `kc-toolbar-action` event * with `detail.action` equal to the action id — the same `` descriptor * element that `` uses for its action bar (composition symmetry). */ export const WithCustomToolbarActions: Story = { name: 'Custom Toolbar Actions (kc-action)', render: () => { let el: HTMLElement | undefined; onMount(() => { if (!el) return; el.setAttribute('placeholder', 'Ask anything...'); el.addEventListener('kc-toolbar-action', (e: Event) => { console.log('toolbar action:', (e as CustomEvent<{ action: string }>).detail.action); }); }); return (
(el = e)} style={{ display: 'block', width: '100%' }} > {/* children are invisible data carriers — Shadow DOM hides them. The element reads them via querySelectorAll + MutationObserver and renders a ghost icon button per entry in the left toolbar. Clicking fires kc-action. */}

Open the browser console to see kc-action events when you click the extra toolbar buttons.

); }, parameters: { docs: { source: { code: CUSTOM_TOOLBAR_SNIPPET, language: 'html' } } }, }; /** Composition: place **``** children inside `` * to declare slash commands without setting the `slashCommands` JS property. * Type `/` in the input to open the palette. Each `` child maps: * `command` attr → id, textContent → label, `description` attr → description. * Selection fires `kc-slash-select` with `detail.command`. * Prop (`slashCommands`) and declarative children are merged — prop items first. */ export const DeclarativeSlashCommands: Story = { name: 'Declarative Slash Commands (kc-slash-command)', render: () => { let el: HTMLElement | undefined; onMount(() => { if (!el) return; el.setAttribute('placeholder', 'Type / to open the command palette…'); el.addEventListener('kc-slash-select', (e: Event) => { console.log('slash selected:', (e as CustomEvent<{ command: unknown }>).detail.command); }); }); return (
(el = e)} style={{ display: 'block', width: '100%' }} > {/* children are invisible data carriers — Shadow DOM hides them. The element reads them via querySelectorAll + MutationObserver. command attr → id, textContent → label, description attr → description. */} summarize translate explain

Type / in the input to open the command palette. Open the browser console to see kc-slash-select events on selection.

); }, parameters: { docs: { source: { code: SLASH_COMMAND_SNIPPET, language: 'html' } } }, };