import { useMemo } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/prism"; import type { DisplayBlock, DiffBlock, TodoBlock, BriefBlock, ShellBlock } from "shared/legacy-sdk"; import { cn } from "@/lib/utils"; import * as Diff from "diff"; function useIsDark(): boolean { return typeof document !== "undefined" && document.documentElement.classList.contains("dark"); } interface DiffBlockProps { block: DiffBlock; maxHeight?: string; } interface DiffPart { value: string; added?: boolean; removed?: boolean; } function renderDiffLine(parts: DiffPart[], type: "added" | "removed"): React.ReactNode { return parts.map((part, i) => { if (type === "removed") { if (part.added) return null; return ( {part.value} ); } else { if (part.removed) return null; return ( {part.value} ); } }); } // Extract diff computation to a pure function for memoization function computeDiffLines(oldText: string, newText: string): { oldLines: DiffPart[][]; newLines: DiffPart[][] } { const diffParts = Diff.diffWords(oldText, newText); const oldLines: DiffPart[][] = []; const newLines: DiffPart[][] = []; let currentOldLine: DiffPart[] = []; let currentNewLine: DiffPart[] = []; for (const part of diffParts) { const lines = part.value.split("\n"); lines.forEach((line, lineIndex) => { const isLastLine = lineIndex === lines.length - 1; const partForLine: DiffPart = { value: line, added: part.added, removed: part.removed, }; if (!part.added) { currentOldLine.push(partForLine); } if (!part.removed) { currentNewLine.push(partForLine); } if (!isLastLine) { if (!part.added) { oldLines.push(currentOldLine); currentOldLine = []; } if (!part.removed) { newLines.push(currentNewLine); currentNewLine = []; } } }); } if (currentOldLine.length > 0) oldLines.push(currentOldLine); if (currentNewLine.length > 0) newLines.push(currentNewLine); return { oldLines, newLines }; } export function DiffBlockView({ block, maxHeight = "max-h-40" }: DiffBlockProps) { const fileName = block.path.split("/").pop() || block.path; const hasOld = block.old_text.length > 0; const hasNew = block.new_text.length > 0; const { oldLines, newLines } = useMemo(() => computeDiffLines(block.old_text, block.new_text), [block.old_text, block.new_text]); return (
{fileName}
{hasOld && (
{oldLines.map((lineParts, i) => (
- {renderDiffLine(lineParts, "removed") || " "}
))}
)} {hasNew && (
{newLines.map((lineParts, i) => (
+ {renderDiffLine(lineParts, "added") || " "}
))}
)}
); } interface TodoBlockProps { block: TodoBlock; } export function TodoBlockView({ block }: TodoBlockProps) { return (
{block.items.map((item, i) => (
{item.title}
))}
); } interface BriefBlockProps { block: BriefBlock; } export function BriefBlockView({ block }: BriefBlockProps) { return
{block.text}
; } interface ShellBlockProps { block: ShellBlock; maxHeight?: string; } export function ShellBlockView({ block, maxHeight = "max-h-40" }: ShellBlockProps) { console.log("ShellBlockView render", { block }); const isDark = useIsDark(); const language = block.language || "bash"; return (
$ Shell Command
{block.command}
); } interface DisplayBlockViewProps { block: DisplayBlock; maxHeight?: string; } export function DisplayBlockView({ block, maxHeight }: DisplayBlockViewProps) { switch (block.type) { case "diff": return ; case "todo": return ; case "brief": return ; default: return null; } } interface DisplayBlocksProps { blocks: DisplayBlock[]; maxHeight?: string; className?: string; } export function DisplayBlocks({ blocks, maxHeight, className }: DisplayBlocksProps) { if (!blocks || blocks.length === 0) { return null; } return (
{blocks.map((block, i) => ( ))}
); }