"use client"; import { memo, useEffect, useState, type ReactNode } from "react"; import { prefetchOnIdle } from "@/components/chat-chunk-prefetch"; import { plainParagraphs } from "@/components/chat-message-paragraphs"; import { TechniqueAuditDisclosure } from "@/components/technique-audit-disclosure"; import { splitStableMarkdown } from "@/lib/chat-markdown-split"; import type { TechniqueAuditRow } from "@/lib/consultation-agent-events"; import { resolveTechniqueAuditRows, splitSpokenAnswerAndTechniqueAudit, } from "@/lib/consultation-technique-audit"; type MarkdownRenderer = (text: string) => ReactNode; let markdownRenderer: MarkdownRenderer | undefined; let markdownRendererRequest: Promise | undefined; function loadMarkdownRenderer() { markdownRendererRequest ??= import("@/components/chat-markdown-view").then((module) => { markdownRenderer = module.renderChatMarkdown; return module.renderChatMarkdown; }); return markdownRendererRequest; } prefetchOnIdle(loadMarkdownRenderer); function useMarkdownRenderer() { const [renderer, setRenderer] = useState(() => markdownRenderer); useEffect(() => { if (renderer) return; let active = true; void loadMarkdownRenderer().then((loaded) => { if (active) setRenderer(() => loaded); }); return () => { active = false; }; }, [renderer]); return renderer; } function renderProse(text: string, renderMarkdown: MarkdownRenderer | undefined): ReactNode { if (!text) return null; return renderMarkdown ? renderMarkdown(text) : (plainParagraphs(text) ?? []).map((paragraph, index) => (

{paragraph}

)); } /** * The completed part of a streaming answer. `memo` keeps React from calling the * markdown parser again while `text` is unchanged, so a frame that only grew * the tail costs one small parse instead of one over the whole answer. */ const StableMarkdownPrefix = memo(function StableMarkdownPrefix({ text, renderMarkdown, }: Readonly<{ text: string; renderMarkdown: MarkdownRenderer | undefined; }>) { return <>{renderProse(text, renderMarkdown)}; }); export function StreamingMarkdown({ text, renderMarkdown, }: Readonly<{ text: string; renderMarkdown: MarkdownRenderer | undefined; }>) { const split = splitStableMarkdown(text); return ( <> {split.stable ? : null} {renderProse(split.tail, renderMarkdown)} ); } export function ChatMessageContent({ text, auditRows, streaming = false, }: { text: string; auditRows?: readonly TechniqueAuditRow[]; streaming?: boolean; }) { const renderMarkdown = useMarkdownRenderer(); const split = splitSpokenAnswerAndTechniqueAudit(text); const rows = resolveTechniqueAuditRows(split, auditRows); const spoken = split.spoken; const showFoldedAudit = rows.length > 0 && split.rows.length === 0; return (
{spoken ? (
{streaming ? : renderMarkdown ? renderMarkdown(spoken) : (plainParagraphs(spoken) ?? []).map((paragraph, index) => (

{paragraph}

))}
) : null} {showFoldedAudit ? : null}
); }