"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) => { const target = event.target as HTMLElement; const link = target.closest("a"); if (!link) {return;} const href = link.getAttribute("href"); if (!href) {return;} if (href.startsWith("#")) { event.preventDefault(); event.stopPropagation(); const slug = href.slice(1); const container = (event.currentTarget as HTMLElement); const allHeadings = Array.from(container.querySelectorAll("h1, h2, h3, h4, h5, h6")); const match = allHeadings.find((h) => { const text = (h.textContent || "").trim().toLowerCase() .replace(/[^\w\s-]/g, "").replace(/\s+/g, "-"); return text === slug; }) ?? container.querySelector(`[id="${CSS.escape(slug)}"]`); match?.scrollIntoView({ behavior: "smooth", block: "start" }); return; } if (!onNavigate) {return;} if (isWorkspaceLink(href) || (!href.startsWith("http://") && !href.startsWith("https://") && !href.startsWith("mailto:"))) { event.preventDefault(); event.stopPropagation(); onNavigate(href); return; } event.preventDefault(); event.stopPropagation(); window.open(href, "_blank", "noopener,noreferrer"); }, [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 (
); })}
); }