/** * Renders memory query/recall text with highlighted entity type annotations, * plus an optional structured entity list when the backend returns entities * in the `context.entities[]` field. * * The backend surfaces entity types in text like: * "Alice (PERSON) -[OWNS]-> Atlas (PROJECT)" * * This component parses those `(TYPE)` annotations and renders them as * small styled badges inline, keeping the rest as plain text. When a * structured `entities` array is provided, it also renders a compact * entity chip bar above the text. */ import type { MemoryRetrievalEntity } from '../../utils/tauriCommands'; interface MemoryTextWithEntitiesProps { text: string; /** Structured entities from `context.entities[]` — shown as chips when present. */ entities?: MemoryRetrievalEntity[]; className?: string; } /** Matches parenthesized entity type annotations like (PERSON), (PROJECT), (ORG). */ const ENTITY_TYPE_RE = /\(([A-Z][A-Z0-9_]{1,30})\)/g; /** Deterministic colour palette for entity type badges (hue-shifted). */ const TYPE_COLORS: Record = { PERSON: { bg: 'bg-sky-500/15', text: 'text-sky-300', border: 'border-sky-500/20' }, PROJECT: { bg: 'bg-emerald-500/15', text: 'text-emerald-300', border: 'border-emerald-500/20' }, ORG: { bg: 'bg-amber-500/15', text: 'text-amber-300', border: 'border-amber-500/20' }, ORGANIZATION: { bg: 'bg-amber-500/15', text: 'text-amber-300', border: 'border-amber-500/20' }, TECHNOLOGY: { bg: 'bg-violet-500/15', text: 'text-violet-300', border: 'border-violet-500/20' }, TOOL: { bg: 'bg-violet-500/15', text: 'text-violet-300', border: 'border-violet-500/20' }, LOCATION: { bg: 'bg-rose-500/15', text: 'text-rose-300', border: 'border-rose-500/20' }, EVENT: { bg: 'bg-pink-500/15', text: 'text-pink-300', border: 'border-pink-500/20' }, CONCEPT: { bg: 'bg-teal-500/15', text: 'text-teal-300', border: 'border-teal-500/20' }, }; const DEFAULT_TYPE_COLOR = { bg: 'bg-primary-500/15', text: 'text-primary-300', border: 'border-primary-500/20', }; function colorForType(entityType: string): { bg: string; text: string; border: string } { return TYPE_COLORS[entityType.toUpperCase()] ?? DEFAULT_TYPE_COLOR; } interface TextSegment { kind: 'text' | 'entity-type'; value: string; } function parseEntityAnnotations(text: string): TextSegment[] { const segments: TextSegment[] = []; let lastIndex = 0; for (const match of text.matchAll(ENTITY_TYPE_RE)) { const matchStart = match.index; if (matchStart > lastIndex) { segments.push({ kind: 'text', value: text.slice(lastIndex, matchStart) }); } segments.push({ kind: 'entity-type', value: match[1] }); lastIndex = matchStart + match[0].length; } if (lastIndex < text.length) { segments.push({ kind: 'text', value: text.slice(lastIndex) }); } return segments; } /** Compact chip for a structured entity, showing name + optional type badge. */ function EntityChip({ entity }: { entity: MemoryRetrievalEntity }) { const color = entity.entity_type ? colorForType(entity.entity_type) : DEFAULT_TYPE_COLOR; return ( {entity.name} {entity.entity_type && ( {entity.entity_type} )} ); } export function MemoryTextWithEntities({ text, entities, className }: MemoryTextWithEntitiesProps) { if (!text && (!entities || entities.length === 0)) return null; const hasStructuredEntities = entities && entities.length > 0; const hasInlineAnnotations = /\([A-Z][A-Z0-9_]{1,30}\)/.test(text); return (
{/* Structured entity chips */} {hasStructuredEntities && (
{entities.map((entity, i) => ( ))}
)} {/* Text content with inline entity type annotations */} {text && (
          {hasInlineAnnotations
            ? parseEntityAnnotations(text).map((seg, i) =>
                seg.kind === 'entity-type' ? (
                  
                    {seg.value}
                  
                ) : (
                  {seg.value}
                )
              )
            : text}
        
)}
); }