import { createSignal, onCleanup, onMount } from 'solid-js'; import { defineWebComponent } from './define'; import { ModelSwitcher } from '../components/model-switcher'; import { wireDisclosure } from './disclosure'; import type { DropdownController } from '../ui/dropdown'; import type { ModelOption } from '../types'; interface Props extends Record { /** The selectable models. Set as a JS property (array). */ models: ModelOption[]; /** The currently-selected model id. Defaults to the first model. */ currentModel?: string; /** Drive/observe the dropdown's open state (Shoelace-style: settable + reflected * to the `open` attribute, the dropdown still self-manages on click/keyboard). * Set `el.open = true`, or ``; listen for `kai-open-change`. */ open?: boolean; /** Initial open state on mount (uncontrolled seed). */ defaultOpen?: boolean; /** Disable the trigger — click/keyboard and `show()` no longer open the dropdown. */ disabled?: boolean; } /** Events fired by ``. */ interface Events { /** A model was selected. */ 'kai-model-change': { modelId: string }; /** The model dropdown opened or closed (by click, keyboard, Escape, outside-click, or a method). */ 'kai-open-change': { open: boolean }; } /** * Parse a single light-DOM `` element into a `ModelOption` descriptor. * Attribute mapping: * - `id` → ModelOption.id * - textContent → ModelOption.name * - `provider` → ModelOption.provider (optional) * - `description` → ModelOption.description (optional subtitle) * - `group` → ModelOption.group (optional collapsible section) */ export function parseKaiModelElement(n: Element): ModelOption { return { id: n.getAttribute('id') ?? '', name: n.textContent?.trim() ?? '', provider: n.getAttribute('provider') ?? undefined, description: n.getAttribute('description') ?? undefined, group: n.getAttribute('group') ?? undefined, }; } /** * `` — an event-emitting leaf element. Data in via the * `models` property, selection out via a `kai-model-change` event. Mirrors the * header switcher inside `` as a standalone, composable piece. * * Note: like the underlying primitive, this only renders when more than one * model is provided. * * **How to use:** * * _Property API_ — set `models` as a JS property and listen for `kai-model-change`: * ```js * el.models = [{ id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI' }]; * el.addEventListener('kai-model-change', (e) => console.log(e.detail.modelId)); * ``` * * _Declarative child API_ — compose `` light-DOM children (no JS needed): * ```html * * GPT-4o * GPT-4o mini * * ``` * Each `` child carries `id` (required), `provider` (optional), and * a text label as its `textContent`. Children are light-DOM data carriers hidden * by Shadow DOM. Prop `models` items render first; declarative children follow. */ defineWebComponent('kai-model-switcher', { models: [], currentModel: undefined, open: undefined, defaultOpen: undefined, disabled: undefined, }, (props, ctx) => { const { dispatch, element, flag } = ctx; let api: DropdownController | undefined; // The standard overlay surface: settable+reflecting `open`, kai-open-change, // show/hide/toggle, disabled-gating. See ./disclosure. When <=1 model the inner // Dropdown never mounts, so `api` stays undefined and these methods no-op. wireDisclosure(ctx, () => api, () => props.open); // Read declarative children from light DOM. // Shadow DOM with no suppresses them visually — they're invisible data carriers. const [slottedModels, setSlottedModels] = createSignal([]); onMount(() => { const read = () => { const nodes = [...element.querySelectorAll('kai-model')]; setSlottedModels(nodes.map(parseKaiModelElement)); }; read(); const observer = new MutationObserver(read); observer.observe(element, { childList: true, attributes: true, subtree: true }); onCleanup(() => observer.disconnect()); }); // Merge prop models (first) with declarative children (after). const allModels = () => [...(props.models ?? []), ...slottedModels()]; return ( dispatch('kai-model-change', { modelId })} defaultOpen={flag('defaultOpen')} disabled={flag('disabled')} controllerRef={(a) => (api = a)} /> ); });