import { createPortal } from "react-dom"; import { Children, cloneElement, isValidElement, useCallback, useContext, useRef, useState, type ComponentProps, type ReactNode } from "react"; import { Streamdown, StreamdownContext, extractTableDataFromElement, tableDataToCSV, tableDataToTSV, type CustomRendererProps } from "streamdown"; import { mermaid } from "@streamdown/mermaid"; import { Check, Copy, Download, Maximize2, Table2, Workflow, X } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "@/shared/ui/dialog"; import { createReadingPreview } from "@/shared/ui/reading-preview"; import { copySelectedText } from "./paragraph-copy"; /** Move the existing renderer into a bounded reader. Pan, selection and DOM state survive. */ function MarkdownReading({ title, kind, children, expandedChildren, toolbar }: { title: string; kind: "table" | "diagram"; children: ReactNode; expandedChildren?: ReactNode; toolbar?: (host: HTMLDivElement) => ReactNode; }) { const [open, setOpen] = useState(false); const [host] = useState(() => document.createElement("div")); const inline = useRef(null); const snapshot = useRef(null); const { isAnimating } = useContext(StreamdownContext); const attachInline = useCallback((slot: HTMLDivElement | null) => { inline.current = slot; if (slot && !host.isConnected) slot.append(host); }, [host]); const attachExpanded = useCallback((slot: HTMLDivElement | null) => { if (!inline.current) return; if (slot) { if (kind === "diagram") { snapshot.current?.remove(); snapshot.current = createReadingPreview(host); inline.current.append(snapshot.current); } slot.append(host); } else { snapshot.current?.remove(); snapshot.current = null; inline.current.append(host); } }, [host, kind]); const Icon = kind === "table" ? Table2 : Workflow; return
{title}
{toolbar?.(host)}
{kind === "table" ?
{children}
:
}
{createPortal(expandedChildren ?? children, host)} { event.preventDefault(); event.stopPropagation(); setOpen(false); }}>
{title} {kind === "diagram" ? 可缩放和平移查看图中关系。 : null}
{toolbar?.(host)}
{ if (slot) slot.append(host); } : attachExpanded} className="min-w-0" />
; } function MarkdownTableActions({ host }: { host: HTMLDivElement }) { const [copied, setCopied] = useState(false); const [error, setError] = useState(false); const getData = () => { const table = host.querySelector("table"); return table ? extractTableDataFromElement(table) : null; }; return <> {error ? 复制失败,请重试 : null} ; } export function ReadableMarkdownTable({ children, className, node: _node, ...props }: ComponentProps<"table"> & { node?: unknown }) { const table =
{children}
; return }> {table} ; } function withoutExplicitIds(children: ReactNode): ReactNode { return Children.map(children, (child) => { if (!isValidElement<{ id?: string; children?: ReactNode }>(child)) return child; return cloneElement(child, { id: undefined, ...(child.props.children !== undefined ? { children: withoutExplicitIds(child.props.children) } : {}) }); }); } export function ReadableMarkdownDiagram({ code, isIncomplete }: CustomRendererProps) { return {isIncomplete ?

图示仍在生成…

: {`\`\`\`mermaid\n${code}\n\`\`\``}}
; }