@kitn.ai/ui
Version:
Framework-agnostic, Shadow-DOM web components for building AI chat interfaces — works in React, Vue, Angular, Svelte, or plain HTML. Authored in SolidJS.
66 lines (65 loc) • 3.66 kB
TypeScript
import { JSX, Accessor } from 'solid-js';
import { CompareSelection, ResponseCompareData } from './response-compare-types';
export type { CompareCandidate, ComparePair, CompareCollapse, ResponseCompareData, CompareSelection, } from './response-compare-types';
export { normalizeCandidates, buildSelection, isAnyStreaming } from './response-compare-types';
export type CompareLayout = 'auto' | 'columns' | 'tabs';
/** Imperative handle exposed via `controllerRef` — surfaces the compare's latent
* capabilities (commit a pick by candidate id, focus the roving tab stop) so the
* `<kai-compare>` facade can forward them as instance methods (Pattern C). */
export interface ResponseCompareController {
/** Commit a pick by candidate id — same path as the "Pick this" button: emits
* onSelect + optimistically collapses (single-shot; inert while streaming or
* already resolved). No-op for an unknown id. */
select(candidateId: string): void;
/** Focus the current roving tab stop (the focused candidate's "Pick this" radio). */
focus(options?: FocusOptions): void;
}
export interface ResolvedController<T> {
value: Accessor<T | undefined>;
isResolved: Accessor<boolean>;
isOptimistic: Accessor<boolean>;
setLocal: (v: T) => void;
}
export declare function useResolved<T>(opts: {
prop: Accessor<T | undefined>;
data: Accessor<unknown>;
}): ResolvedController<T>;
export interface ResponseCompareProps {
/** The compare definition (prompt + the two candidates). */
data?: ResponseCompareData;
/** Stable id correlating every emitted event. */
compareId?: string;
/** Re-hydrate / control the selection. Renders the collapsed winner. Set as a
* JS property: `el.selection = { chosenId, rejectedIds }`. */
selection?: CompareSelection;
/** Layout. `'columns'` = side-by-side; `'tabs'` = one candidate at a time with
* pills to switch; `'auto'` (default) uses a CONTAINER query — columns when the
* component is ≥640px wide, tabs when narrower (e.g. on a phone). */
layout?: CompareLayout;
class?: string;
/** Receive the imperative controller once mounted. The `<kai-compare>` facade
* forwards these as element methods (select/focus). */
controllerRef?: (controller: ResponseCompareController) => void;
/** Fired when the user commits a pick. */
onSelect?: (sel: CompareSelection) => void;
/** Fired once both candidates have settled (stopped streaming) and a valid
* definition is shown. */
onReady?: () => void;
/** Fired for an unusable definition (inline error state). */
onError?: (message: string) => void;
}
/**
* `ResponseCompare` — a dual-response comparison. Two assistant candidates for the
* same prompt render side-by-side (or as tabs), each via `MessageBody` so a
* candidate reads exactly like an assistant message (reasoning + tools +
* attachments + markdown). The pick is a COMMIT, not a Submit: clicking "Pick this"
* (or Enter on the focused column) computes `{ chosenId, rejectedIds:[other], at }`,
* optimistically collapses to the chosen candidate's body, and calls `onSelect` for
* the consumer to send a `(prompt, chosen, rejected)` preference pair. Single-shot.
*
* While EITHER candidate is `streaming`, both pick controls are disabled and the
* streaming column shows a `TextShimmer`; `onReady` fires once both settle. The two
* columns are a WAI-ARIA radiogroup with roving tabindex (Arrow keys move A↔B,
* Enter/Space picks). Emits `onError` for a malformed definition.
*/
export declare function ResponseCompare(props: ResponseCompareProps): JSX.Element;