import { AI_RICH_BLOCK_HANDLERS, type AiMessageChartSegment, type AiMessageHtmlSegment, type AiRichBlockHandler } from "@/lib/ai/richContent/aiRichContent"; export interface AiMessageTextSegment { type: "text"; content: string; html: string; } export interface AiMessageCodeSegment { type: "code"; content: string; lang: string; html: string; isSql: boolean; /** True while the closing fence is missing, i.e. the code is incomplete and must not be executed. */ pending: boolean; } export type AiMessageRenderSegment = AiMessageTextSegment | AiMessageCodeSegment | AiMessageChartSegment | AiMessageHtmlSegment; interface MessageSegment { type: "text" | "code"; content: string; lang?: string; /** Code segments only: whether the closing fence has arrived. */ closed?: boolean; } export interface AiMessageRenderOptions { /** Set while the message is still streaming: the trailing segment keeps growing. */ streaming?: boolean; } export interface AiMessageRendererOptions { maxEntries?: number; maxCacheableChars?: number; maxSegmentEntries?: number; maxCacheChars?: number; markdown: (text: string) => string; highlightCode?: (content: string, lang: string) => string; /** Resolves a fenced raw lang to a rich handler (chart-json, ...). */ richHandlers?: Record; } const DEFAULT_MAX_ENTRIES = 100; const DEFAULT_MAX_CACHEABLE_CHARS = 20_000; const DEFAULT_MAX_SEGMENT_ENTRIES = 300; const DEFAULT_MAX_CACHE_CHARS = 400_000; /** Streaming text is flushed into a cached block only once it is long enough to be worth a cache entry. */ const STREAM_BLOCK_MIN_CHARS = 240; const BLANK_LINE_RE = /\n{2,}/g; const FENCE_LINE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/; const INLINE_LANGUAGE_FENCE_RE = /^(.*?)(```[a-zA-Z0-9_+.-]+)[ \t]*(\r?)$/; const MARKDOWN_CONTAINER_PREFIX_RE = /^[>*+\d.)-]+$/; const STANDALONE_CLOSING_FENCE_RE = /^```[ \t]*\r?$/; // Definitions inside block containers still apply to the whole document. Container indentation // may exceed three columns, so this prefix is intentionally conservative: a false positive only // disables streaming splits, while a false negative changes reference-link rendering. const LINK_REFERENCE_RE = /^(?:[ \t]*(?:>[ \t]?|(?:[*+-]|\d{1,9}[.)])[ \t]+))*[ \t]*\[(?:\\.|[^\]\\]){1,999}\]:/m; // Raw HTML blocks stay open across blank lines, which no block boundary may cut: // comments, processing instructions, declarations, CDATA and the raw-text elements. const RAW_HTML_BLOCK_RE = /