"use client"; import { useState, useCallback, type MouseEvent as ReactMouseEvent } from "react"; import dynamic from "next/dynamic"; import { splitReportBlocks, hasReportBlocks } from "@/lib/report-blocks"; import { splitDiffBlocks, hasDiffBlocks } from "@/lib/diff-blocks"; import { isWorkspaceLink } from "@/lib/workspace-links"; import { DiffCard } from "../diff-viewer"; import type { TreeNode, MentionSearchFn } from "./slash-command"; // Load markdown renderer client-only to avoid SSR issues with ESM-only packages const MarkdownContent = dynamic( () => import("./markdown-content").then((mod) => mod.MarkdownContent), { ssr: false, loading: () => (
), }, ); // Lazy-load ReportCard (uses Recharts which is heavy) const ReportCard = dynamic( () => import("../charts/report-card").then((m) => ({ default: m.ReportCard })), { ssr: false, loading: () => (
), }, ); // Lazy-load the Tiptap-based editor (heavy -- keep out of initial bundle) const MarkdownEditor = dynamic( () => import("./markdown-editor").then((m) => ({ default: m.MarkdownEditor })), { ssr: false, loading: () => (
), }, ); type DocumentViewProps = { content: string; title?: string; filePath?: string; tree?: TreeNode[]; onSave?: () => void; onNavigate?: (path: string) => void; searchFn?: MentionSearchFn; }; export function DocumentView({ content, title, filePath, tree, onSave, onNavigate, searchFn, }: DocumentViewProps) { const [editMode, setEditMode] = useState(!!filePath); // Strip YAML frontmatter if present const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, ""); // Extract title from first H1 if no title provided const h1Match = body.match(/^#\s+(.+)/m); const displayTitle = title ?? h1Match?.[1]; const markdownBody = displayTitle && h1Match ? body.replace(/^#\s+.+\n?/, "") : body; // If we have a filePath and editing is enabled, render the Tiptap editor if (editMode && filePath) { return (
); } // Check if the markdown contains embedded rich blocks (reports or diffs) const hasRichBlocks = hasReportBlocks(markdownBody) || hasDiffBlocks(markdownBody); // Intercept workspace-internal links in read mode (delegated click handler) const handleLinkClick = useCallback( (event: ReactMouseEvent) => { if (!onNavigate) {return;} const target = event.target as HTMLElement; const link = target.closest("a"); if (!link) {return;} const href = link.getAttribute("href"); if (!href) {return;} if (isWorkspaceLink(href)) { event.preventDefault(); event.stopPropagation(); onNavigate(href); } }, [onNavigate], ); return ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
{/* Header row with title + edit button */}
{displayTitle && (

{displayTitle}

)} {filePath && ( )}
{hasRichBlocks ? ( ) : (
)}
); } /** * Renders markdown content that contains embedded rich blocks (reports and diffs). * Splits the content into alternating markdown, chart, and diff sections. */ function EmbeddedRichContent({ content }: { content: string }) { // First split on report blocks, then further split text segments on diff blocks const reportSegments = splitReportBlocks(content); type RichSegment = | { type: "text"; text: string } | { type: "report-artifact"; config: import("@/lib/report-blocks").ReportConfig } | { type: "diff-artifact"; diff: string }; const segments: RichSegment[] = []; for (const seg of reportSegments) { if (seg.type === "text" && hasDiffBlocks(seg.text)) { for (const ds of splitDiffBlocks(seg.text)) { segments.push(ds); } } else { segments.push(seg as RichSegment); } } return (
{segments.map((segment, index) => { if (segment.type === "report-artifact") { return (
); } if (segment.type === "diff-artifact") { return (
); } // Text segment -- render as markdown return (
); })}
); }