/** * The pieces of `ExtensionComboBuilder`: the committed-combo list * (`CommittedComboList`), the under-construction combo editor (`ComboDraftEditor`), * and the shared combo helpers. The builder owns the state and composes these. */ import type { Extension, PresetExtensionElement } from '@tai42/api-client'; import { Fragment, type ReactNode } from 'react'; import { comboElementNames } from '../extension-combos'; import { SchemaEditor, type SchemaEditorChange } from '../schema-editor'; import { Badge } from './badge'; import { ExtensionPicker } from './extension-picker'; import { CloseIcon, XCircleIcon } from './icons'; import { Button } from './primitives'; /** The one config-taking extension this builder authors inline. */ export const OUTPUT_SCHEMA = 'output_schema'; /** Two combos are equal when their NAME sequences match (config is not part of identity). */ export function sameNames(a: readonly string[], b: readonly string[]): boolean { return a.length === b.length && a.every((name, index) => name === b[index]); } /** The stored `output_schema` config schema for a combo, if it carries one. */ export function outputSchemaConfig( combo: readonly PresetExtensionElement[], ): Record | null { for (const element of combo) { if (typeof element !== 'string' && element.name === OUTPUT_SCHEMA) { const schema = element.config.schema; return typeof schema === 'object' && schema !== null && !Array.isArray(schema) ? (schema as Record) : {}; } } return null; } /** The element names in a combo that are absent from the known catalog. */ export function unknownNames( combo: readonly PresetExtensionElement[], availableNames: ReadonlySet, ): string[] { return comboElementNames(combo).filter((name) => !availableNames.has(name)); } /** One committed combo's row: its name badges and the Edit / Remove pair, with an * inline unknown-name note. */ function CommittedComboRow({ combo, index, editing, disabled, unknown, onEdit, onRemove, }: { readonly combo: readonly PresetExtensionElement[]; readonly index: number; readonly editing: number | null; readonly disabled: boolean | undefined; readonly unknown: readonly string[]; readonly onEdit: (index: number) => void; readonly onRemove: (index: number) => void; }): ReactNode { const names = comboElementNames(combo); return (
  • {/* The badges take the row's spare width so the Edit/Remove pair pins to a stable trailing edge across combos of varied length. */} {names.map((name, position) => ( {position > 0 ? ( + ) : null} {name} ))}
    {unknown.length > 0 ? (

    {unknown.length === 1 ? 'Unknown extension: ' : 'Unknown extensions: '} {unknown.join(', ')}.

    ) : null}
  • ); } /** The current combos, each a row of name badges with per-combo Edit and Remove. */ export function CommittedComboList({ value, availableReady, availableNames, editing, disabled, onEdit, onRemove, }: { readonly value: readonly PresetExtensionElement[][]; readonly availableReady: boolean; readonly availableNames: ReadonlySet; readonly editing: number | null; readonly disabled: boolean | undefined; readonly onEdit: (index: number) => void; readonly onRemove: (index: number) => void; }): ReactNode { if (value.length === 0) return

    No extension sets.

    ; return (
      {value.map((combo, index) => ( // The index IS the identity here: the list mutates by append, in-place update, // and remove-by-index (no reorder), and a combo has no stable id of its own — // so an index key is stable and cannot collide. ))}
    ); } /** The combo under construction: the extension picker, the inline output-schema * editor when present, the duplicate note, and the Add/Update + Cancel actions. */ export function ComboDraftEditor({ available, draft, onDraftChange, draftHasOutputSchema, draftConfig, onDraftConfigChange, isDuplicate, canAdd, editing, disabled, idPrefix, onCommit, onReset, }: { readonly available: readonly Extension[]; readonly draft: string[]; readonly onDraftChange: (draft: string[]) => void; readonly draftHasOutputSchema: boolean; readonly draftConfig: SchemaEditorChange; readonly onDraftConfigChange: (change: SchemaEditorChange) => void; readonly isDuplicate: boolean; readonly canAdd: boolean; readonly editing: number | null; readonly disabled: boolean | undefined; readonly idPrefix: string; readonly onCommit: () => void; readonly onReset: () => void; }): ReactNode { return (
    {draftHasOutputSchema ? ( ) : null} {isDuplicate ? (

    This extension set is already added.

    ) : null}
    {editing !== null ? ( ) : null}
    ); }