import { type JSX, type Accessor, splitProps, createSignal, For, Show, } from 'solid-js'; import { cn } from '../utils/cn'; import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '../ui/collapsible'; import { ChevronDown, Circle } from 'lucide-solid'; // --- ChainOfThoughtItem --- export interface ChainOfThoughtItemProps extends JSX.HTMLAttributes { children: JSX.Element; } function ChainOfThoughtItem(props: ChainOfThoughtItemProps) { const [local, rest] = splitProps(props, ['children', 'class']); return (
{local.children}
); } // --- ChainOfThoughtTrigger --- export interface ChainOfThoughtTriggerProps { children: JSX.Element; class?: string; leftIcon?: JSX.Element; swapIconOnHover?: boolean; } function ChainOfThoughtTrigger(props: ChainOfThoughtTriggerProps) { const swapOnHover = () => props.swapIconOnHover ?? true; return (
} > {props.leftIcon} {props.children}
); } // --- ChainOfThoughtContent --- export interface ChainOfThoughtContentProps { children: JSX.Element; class?: string; } function ChainOfThoughtContent(props: ChainOfThoughtContentProps) { return (
{props.children}
); } // --- ChainOfThought (Root) --- export interface ChainOfThoughtProps { children: JSX.Element; class?: string; } function ChainOfThought(props: ChainOfThoughtProps) { return (
{props.children}
); } // --- ChainOfThoughtStep --- export interface ChainOfThoughtStepProps { children: JSX.Element; class?: string; isLast?: boolean; /** Controlled open state. When provided, the step's Collapsible is driven by * this value (the parent owns it — used by the Accordion model below). When * OMITTED the step stays uncontrolled, preserving the original behaviour * (every step independently toggleable, starting closed). */ open?: boolean; /** Called with the desired next open state when the user clicks the trigger. * Only meaningful when `open` is also provided (controlled mode); the parent * Accordion routes this through its open-set handler. */ onOpenChange?: (open: boolean) => void; } function ChainOfThoughtStep(props: ChainOfThoughtStepProps) { return ( {props.children}
); } // --- Accordion model (the kai-chain-of-thought interaction surface) --- /** Open-mode: `multiple` (any number of steps open at once — the historical * default) or `single` (at most one open; opening a step closes the others). */ export type ChainOfThoughtType = 'single' | 'multiple'; /** * A reasoning step descriptor. `id` is an OPTIONAL stable key — the Accordion's * open-set keys use `step.id` when present, else `String(index)`. So a consumer * who wants to drive `value`/`defaultValue` (or read `kai-value-change`) by a * meaningful key should set `id`; otherwise the step's position is its key. */ export interface ChainOfThoughtStepData { /** The step's heading (the always-visible trigger). */ label: string; /** Optional expandable detail. */ content?: string; /** Optional stable key for the open-set/value model (defaults to the index). */ id?: string; } /** * Imperative handle exposed via `controllerRef` so the `` * facade can drive/observe the Accordion's open set. Index-based to match the * `expand(index?)`/`collapse(index?)`/`toggle(index?)` method signatures; the * controller resolves the index → step key internally. All setters flow through * the same open-set mutate path as user clicks, so they all emit * `kai-value-change` via `onValueChange`. */ export interface ChainOfThoughtController { /** Current open keys, normalised per `type` (string in single, string[] in multiple). */ value: Accessor; /** Open one step by index, or ALL steps when `index` is omitted. */ expand: (index?: number) => void; /** Close one step by index, or ALL steps when `index` is omitted. */ collapse: (index?: number) => void; /** Flip one step's open state by index. */ toggle: (index?: number) => void; } export interface ChainOfThoughtAccordionProps { /** The reasoning steps to render. */ steps: ChainOfThoughtStepData[]; /** Open mode. Default `multiple` (the historical behaviour). */ type?: ChainOfThoughtType; /** Controlled open step key(s). When provided it WINS over internal state. */ value?: string | string[]; /** Uncontrolled initial open step key(s). Seeds the internal open-set. */ defaultValue?: string | string[]; /** Fired whenever the open set changes (user click OR a controller method). * The single emit point — the facade turns this into `kai-value-change`. The * payload is a string in `single` mode, a string[] in `multiple` mode. */ onValueChange?: (value: string | string[]) => void; /** Receive the imperative controller once set up (Pattern C). */ controllerRef?: (api: ChainOfThoughtController) => void; class?: string; } /** Normalise a `string | string[]` (or undefined) seed to a Set of keys. */ function toKeySet(value: string | string[] | undefined): Set { if (value == null) return new Set(); return new Set(Array.isArray(value) ? value : [value]); } /** * The full Accordion-model root used by ``. Lifts each * step's open state into a controlled/uncontrolled open-set: * - seeded from `defaultValue` (normalised string|string[] → Set); * - when `value` is provided it WINS (controlled); * - a trigger click updates the set respecting `single` vs `multiple` and emits * via `onValueChange` — in single mode opening a step closes the others; * - each step's Collapsible is driven (controlled) by its membership in the set. * * With NO new props (type defaults to `multiple`, no value/defaultValue), every * step starts closed and is independently toggleable — identical to the old * behaviour, just with the open state lifted here so methods/events can observe * and drive it. */ function ChainOfThoughtAccordion(props: ChainOfThoughtAccordionProps) { const [internal, setInternal] = createSignal>(toKeySet(props.defaultValue)); const type = (): ChainOfThoughtType => props.type ?? 'multiple'; // The step key: explicit id when present, else the index as a string. const keyOf = (step: ChainOfThoughtStepData, index: number) => step.id ?? String(index); // Controlled `value` wins; otherwise the internal signal owns the open-set. const isControlled = () => props.value !== undefined; const openSet = (): Set => (isControlled() ? toKeySet(props.value) : internal()); const isOpen = (key: string) => openSet().has(key); // Shape the open-set as the public value: a single string (or '') in single // mode, a string[] in multiple mode. const toValue = (set: Set): string | string[] => { if (type() === 'single') { const first = set.values().next(); return first.done ? '' : first.value; } return [...set]; }; // The single mutate path — shared by trigger clicks and controller methods. // Computes the next open-set, writes it (uncontrolled only) and ALWAYS emits // onValueChange so both user + programmatic changes flow to kai-value-change. const commit = (next: Set) => { const cur = openSet(); // No-op if the set is unchanged (avoids redundant events). if (cur.size === next.size && [...cur].every((k) => next.has(k))) return; if (!isControlled()) setInternal(next); props.onValueChange?.(toValue(next)); }; // Set a single key's open state, respecting single vs multiple. const setKeyOpen = (key: string, open: boolean) => { if (type() === 'single') { commit(open ? new Set([key]) : new Set()); return; } const next = new Set(openSet()); if (open) next.add(key); else next.delete(key); commit(next); }; // Resolve a step index → key (undefined when out of range). const keyAt = (index: number): string | undefined => { const step = props.steps[index]; return step ? keyOf(step, index) : undefined; }; const allKeys = (): string[] => props.steps.map((s, i) => keyOf(s, i)); // --- Controller (Pattern C): hand the facade an index-based handle. Every // setter routes through commit() so methods + clicks share one emit point. props.controllerRef?.({ value: () => toValue(openSet()), expand: (index?: number) => { if (index === undefined) { // Open all. In single mode only one can be open — keep the last step. if (type() === 'single') { const keys = allKeys(); commit(keys.length ? new Set([keys[keys.length - 1]]) : new Set()); } else { commit(new Set(allKeys())); } return; } const key = keyAt(index); if (key !== undefined) setKeyOpen(key, true); }, collapse: (index?: number) => { if (index === undefined) { commit(new Set()); return; } const key = keyAt(index); if (key !== undefined) setKeyOpen(key, false); }, toggle: (index?: number) => { if (index === undefined) return; const key = keyAt(index); if (key !== undefined) setKeyOpen(key, !isOpen(key)); }, }); return ( {(step, i) => { const key = () => keyOf(step, i()); return ( setKeyOpen(key(), open)} > {step.label} {step.content} ); }} ); } export { ChainOfThought, ChainOfThoughtStep, ChainOfThoughtTrigger, ChainOfThoughtContent, ChainOfThoughtItem, ChainOfThoughtAccordion, };