import { For, Show, createSignal, onMount, onCleanup } from 'solid-js'; import { defineWebComponent } from './define'; import { ChatConfig, useChatConfig, type ProseSize } from '../primitives/chat-config'; import { Message, MessageAvatar, MessageContent, MessageActionBar } from '../components/message'; import { Reasoning, ReasoningTrigger, ReasoningContent } from '../components/reasoning'; import { Tool } from '../components/tool'; import { Attachments, Attachment, AttachmentPreview, AttachmentInfo } from '../components/attachments'; import type { ChatMessage } from './chat-types'; interface Props extends Record { /** The full message object. Set as a JS property. */ message?: ChatMessage; /** Convenience for simple cases when not passing a `message` object. */ role?: 'user' | 'assistant'; /** Convenience content (used when `message` is not set). */ content?: string; /** Force markdown on/off. Defaults to on for assistant, off for user. */ markdown?: boolean; /** Text/markdown sizing for the message body. */ proseSize?: ProseSize; /** Shiki theme name used for fenced code blocks in the content. */ codeTheme?: string; /** Disable syntax highlighting for code blocks (no Shiki loads). */ codeHighlight?: boolean; /** Whether the action bar is always visible (`'always'`, default) or only * revealed on hover of the message row (`'hover'`). */ actionsReveal?: 'always' | 'hover'; /** Convenience avatar image URL (used when `message.avatar` is not set). */ avatarSrc?: string; /** Convenience avatar fallback text (used when `message.avatar` is not set). */ avatarFallback?: string; } /** Events fired by ``. */ interface Events { /** An action button was clicked. `action` is the built-in name or custom id. */ 'kc-message-action': { messageId: string; action: string }; } /** * `` — a single message row: markdown/plain content, reasoning, * tool calls, attachments, and action buttons, rendered from one `message` * object (the same shape `` uses per message). The keystone of the * "compose your own message list" pattern. Emits `kc-message-action`. */ defineWebComponent('kc-message', { message: undefined, role: 'assistant', content: undefined, markdown: undefined, proseSize: 'sm', codeTheme: 'github-dark-dimmed', codeHighlight: true, actionsReveal: 'always', avatarSrc: undefined, avatarFallback: undefined, }, (props, { dispatch, flag, element }) => { const outer = useChatConfig(); const msg = (): ChatMessage => props.message ?? { id: 'message', role: props.role ?? 'assistant', content: props.content ?? '' }; // Read declarative children from light DOM. // Shadow DOM with no suppresses them visually — they're invisible data carriers. const [slottedActions, setSlottedActions] = createSignal([]); onMount(() => { const read = () => { const nodes = [...element.querySelectorAll('kc-action')]; setSlottedActions(nodes.map(n => ({ id: n.id || n.getAttribute('action') || '', label: n.textContent?.trim() || n.getAttribute('label') || n.id || '', icon: n.getAttribute('icon') ?? undefined, tooltip: n.getAttribute('tooltip') ?? undefined, }))); }; read(); const observer = new MutationObserver(read); observer.observe(element, { childList: true, attributes: true, subtree: true }); onCleanup(() => observer.disconnect()); }); const isUser = () => msg().role === 'user'; const avatar = () => msg().avatar ?? (props.avatarSrc || props.avatarFallback ? { src: props.avatarSrc as string | undefined, fallback: props.avatarFallback as string | undefined } : undefined); const reveal = () => (props.actionsReveal === 'hover' ? 'hover' : 'always'); // markdown: explicit prop/attribute wins; otherwise default by role. const markdownExplicit = () => element.hasAttribute('markdown') || props.markdown === true || props.markdown === false; const useMarkdown = () => (markdownExplicit() ? flag('markdown') : !isUser()); const body = () => ( <> {msg().reasoning!.label ?? 'Reasoning'} {msg().reasoning!.text} {(tp) => } {(att) => ( )} {msg().content} 0 || slottedActions().length > 0}> dispatch('kc-message-action', { messageId: msg().id, action })} /> ); // Row carries `group` so a hover-revealed action bar fades in on row hover. const rowGroup = () => (reveal() === 'hover' ? 'group ' : ''); return ( {body()} } > {(av) => (
{body()}
)}
); });