import { createSignal, onCleanup, onMount } from 'solid-js'; import { defineWebComponent } from './define'; import { ChatThread, type ChatThreadProps, type ChatThreadContextUsage } from '../components/chat-thread'; import type { AttachmentData } from '../components/attachments'; import type { SlashCommandItem } from '../components/slash-command'; import type { ProseSize } from '../primitives/chat-config'; import type { ModelOption } from '../types'; type Props = Omit & Record; interface Events { /** User submitted a message. */ 'kc-submit': { value: string; attachments: AttachmentData[] }; /** Fired on every input change. */ 'kc-value-change': { value: string }; /** A suggestion chip was clicked (only in `suggestion-mode="fill"`). */ 'kc-suggestion-click': { value: string }; /** An action button on a message was clicked. `action` is the built-in name or custom id. */ 'kc-message-action': { messageId: string; action: string }; /** The header model switcher changed. */ 'kc-model-change': { modelId: string }; /** A slash command was chosen from the palette. */ 'kc-slash-select': { command: SlashCommandItem }; /** The Search button was clicked. */ 'kc-search': Record; /** The Mic / voice button was clicked. */ 'kc-voice': Record; } defineWebComponent('kc-chat', { messages: [], value: undefined, placeholder: 'Send a message...', loading: false, suggestions: undefined, suggestionMode: 'submit', persistSuggestions: false, proseSize: 'sm', codeTheme: 'github-dark-dimmed', codeHighlight: true, chatTitle: undefined, models: undefined, currentModel: undefined, context: undefined, scrollButton: true, search: false, voice: false, slashCommands: undefined, slashActiveIds: undefined, slashCompact: false, actionsReveal: 'always', }, (props, { dispatch, flag, element }) => { // Detect consumer-projected header controls so the header opens for them even // without a title/models/context. Mirrors the light-DOM read pattern. const [hasHeaderStart, setHasHeaderStart] = createSignal(false); const [hasHeaderEnd, setHasHeaderEnd] = createSignal(false); onMount(() => { const read = () => { setHasHeaderStart(!!element.querySelector(':scope > [slot="header-start"]')); setHasHeaderEnd(!!element.querySelector(':scope > [slot="header-end"]')); }; read(); const observer = new MutationObserver(read); observer.observe(element, { childList: true }); onCleanup(() => observer.disconnect()); }); return ( dispatch('kc-value-change', { value })} onSubmit={(detail) => dispatch('kc-submit', detail)} onSuggestionClick={(value) => dispatch('kc-suggestion-click', { value })} onModelChange={(modelId) => dispatch('kc-model-change', { modelId })} onMessageAction={(detail) => dispatch('kc-message-action', detail)} onSearch={() => dispatch('kc-search', {})} onVoice={() => dispatch('kc-voice', {})} onSlashSelect={(command) => dispatch('kc-slash-select', { command })} headerStart={hasHeaderStart()} headerEnd={hasHeaderEnd()} /> ); });