@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.
50 lines (47 loc) • 1.71 kB
text/typescript
export interface EntityRef {
kind: string;
id: string;
label: string;
icon?: string;
promptText?: string;
data?: Record<string, unknown>;
}
export type Segment =
| { type: 'text'; text: string }
| { type: 'entity'; entity: EntityRef };
export type ComposerDoc = Segment[];
export function normalizeValue(value: string | ComposerDoc | null | undefined): ComposerDoc {
if (value == null) return [];
if (typeof value === 'string') return value.length ? [{ type: 'text', text: value }] : [];
const out: ComposerDoc = [];
for (const seg of value) {
if (seg.type === 'text') {
if (!seg.text) continue;
const last = out[out.length - 1];
// Replace (don't mutate) the prior segment so every object in the returned
// doc is freshly built and never an aliased/held reference.
if (last && last.type === 'text') out[out.length - 1] = { type: 'text', text: last.text + seg.text };
else out.push({ type: 'text', text: seg.text });
} else {
out.push({ type: 'entity', entity: seg.entity });
}
}
return out;
}
export function serializeToText(
doc: ComposerDoc,
opts?: { entity?: (e: EntityRef) => string },
): string {
let out = '';
for (const seg of doc) {
if (seg.type === 'text') out += seg.text;
else out += opts?.entity ? opts.entity(seg.entity) : (seg.entity.promptText ?? seg.entity.label);
}
return out;
}
export function entitiesOf(doc: ComposerDoc): EntityRef[] {
return doc.filter((s): s is Extract<Segment, { type: 'entity' }> => s.type === 'entity').map((s) => s.entity);
}
export function docIsEmpty(doc: ComposerDoc): boolean {
return doc.every((s) => s.type === 'text' && s.text.length === 0);
}