import { useState, useRef, useEffect } from "react"; import type { BodyElement } from "../../lib/api"; import { ChartRenderer } from "../charts/ChartRenderer"; import { ElementContextMenu } from "./ElementContextMenu"; interface BodyRendererProps { elements: BodyElement[]; isEditing?: boolean; onUpdate?: (index: number, element: BodyElement) => void; onReorder?: (fromIndex: number, toIndex: number) => void; onEditingChange?: (isEditing: boolean) => void; hasPendingRemoteUpdate?: boolean; onOverwrite?: () => void; onFocusElement?: (index: number) => void; onDeleteElement?: (index: number) => void; onDuplicateElement?: (index: number) => void; onConvertElement?: (index: number, toType: string) => void; onGenerateFromElement?: (index: number, action: string) => void; onCreateTaskFromElement?: (index: number, selectedText?: string) => void; } export function BodyRenderer({ elements, isEditing = false, onUpdate, onReorder, onEditingChange, hasPendingRemoteUpdate, onOverwrite, onFocusElement, onDeleteElement, onDuplicateElement, onConvertElement, onGenerateFromElement, onCreateTaskFromElement, }: BodyRendererProps) { const [draggedIndex, setDraggedIndex] = useState(null); const [dragOverIndex, setDragOverIndex] = useState(null); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; elementIndex: number; selectedText?: string; } | null>(null); const handleContextMenu = (index: number) => (e: React.MouseEvent) => { e.preventDefault(); // Get any selected text const selection = window.getSelection(); const selectedText = selection?.toString().trim() || undefined; setContextMenu({ x: e.clientX, y: e.clientY, elementIndex: index, selectedText, }); }; const closeContextMenu = () => { setContextMenu(null); }; if (elements.length === 0) { return (
No content yet. Use the CLI below to add content.
); } const handleDragStart = (index: number) => (e: React.DragEvent) => { setDraggedIndex(index); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", index.toString()); }; const handleDragOver = (index: number) => (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; if (draggedIndex !== null && draggedIndex !== index) { setDragOverIndex(index); } }; const handleDragLeave = () => { setDragOverIndex(null); }; const handleDrop = (toIndex: number) => (e: React.DragEvent) => { e.preventDefault(); const fromIndex = draggedIndex; setDraggedIndex(null); setDragOverIndex(null); if (fromIndex !== null && fromIndex !== toIndex && onReorder) { onReorder(fromIndex, toIndex); } }; const handleDragEnd = () => { setDraggedIndex(null); setDragOverIndex(null); }; return (
{elements.map((element, index) => (
{/* Drag handle - only show in edit mode */} {isEditing && onReorder && (
)}
onUpdate(index, el) : undefined} onEditingChange={onEditingChange} hasPendingRemoteUpdate={hasPendingRemoteUpdate} onOverwrite={onOverwrite} />
))} {/* Context Menu */} {contextMenu && ( onFocusElement?.(index)} onDelete={(index) => onDeleteElement?.(index)} onDuplicate={(index) => onDuplicateElement?.(index)} onConvert={(index, toType) => onConvertElement?.(index, toType)} onGenerate={(index, action) => onGenerateFromElement?.(index, action)} onCreateTask={(index, selectedText) => onCreateTaskFromElement?.(index, selectedText)} /> )}
); } function BodyElementRenderer({ element, onUpdate, onEditingChange, hasPendingRemoteUpdate, onOverwrite, }: { element: BodyElement; onUpdate?: (element: BodyElement) => void; onEditingChange?: (isEditing: boolean) => void; hasPendingRemoteUpdate?: boolean; onOverwrite?: () => void; }) { switch (element.type) { case "heading": return ( onUpdate({ ...element, text }) : undefined } onEditingChange={onEditingChange} hasPendingRemoteUpdate={hasPendingRemoteUpdate} onOverwrite={onOverwrite} /> ); case "paragraph": return ( onUpdate({ ...element, text }) : undefined } onEditingChange={onEditingChange} hasPendingRemoteUpdate={hasPendingRemoteUpdate} onOverwrite={onOverwrite} /> ); case "code": return ( ); case "list": return ( ); case "chart": return ( ); case "image": return ( ); default: return null; } } function HeadingElement({ level, text, onUpdate, onEditingChange, hasPendingRemoteUpdate, onOverwrite, }: { level: number; text: string; onUpdate?: (text: string) => void; onEditingChange?: (isEditing: boolean) => void; hasPendingRemoteUpdate?: boolean; onOverwrite?: () => void; }) { const [isEditing, setIsEditing] = useState(false); const [editText, setEditText] = useState(text); const inputRef = useRef(null); useEffect(() => { // Only update editText if not currently editing if (!isEditing) { setEditText(text); } }, [text, isEditing]); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isEditing]); const startEditing = () => { setIsEditing(true); onEditingChange?.(true); }; const stopEditing = () => { setIsEditing(false); onEditingChange?.(false); }; const handleSave = () => { // Don't auto-save if there's a pending remote update if (hasPendingRemoteUpdate) return; if (onUpdate && editText.trim() !== text) { onUpdate(editText.trim()); } stopEditing(); }; const handleOverwrite = () => { if (onUpdate && editText.trim() !== text) { onUpdate(editText.trim()); } onOverwrite?.(); stopEditing(); }; const handleCancel = () => { setEditText(text); stopEditing(); }; const handleKeyDown = (e: React.KeyboardEvent) => { // Disable Enter save if there's a pending remote update if (e.key === "Enter" && !hasPendingRemoteUpdate) { handleSave(); } else if (e.key === "Escape") { handleCancel(); } }; const handleBlur = () => { // Don't auto-save on blur if there's a pending remote update if (!hasPendingRemoteUpdate) { handleSave(); } }; const baseClassName = "font-mono text-[#9bc3ff]"; const sizeClasses: Record = { 1: "text-2xl font-bold", 2: "text-xl font-bold", 3: "text-lg font-semibold", 4: "text-base font-semibold", 5: "text-sm font-semibold", 6: "text-xs font-semibold", }; const sizeClass = sizeClasses[level] || sizeClasses[3]; if (isEditing && onUpdate) { return (
setEditText(e.target.value)} onBlur={handleBlur} onKeyDown={handleKeyDown} className={`${baseClassName} ${sizeClass} w-full bg-transparent border-b border-[#3f6fb3] outline-none`} /> {hasPendingRemoteUpdate && (
Remote update pending
)}
); } const HeadingTag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; return ( onUpdate && startEditing()} > {text} ); } function ParagraphElement({ text, onUpdate, onEditingChange, hasPendingRemoteUpdate, onOverwrite, }: { text: string; onUpdate?: (text: string) => void; onEditingChange?: (isEditing: boolean) => void; hasPendingRemoteUpdate?: boolean; onOverwrite?: () => void; }) { const [isEditing, setIsEditing] = useState(false); const [editText, setEditText] = useState(text); const textareaRef = useRef(null); useEffect(() => { // Only update editText if not currently editing if (!isEditing) { setEditText(text); } }, [text, isEditing]); useEffect(() => { if (isEditing && textareaRef.current) { textareaRef.current.focus(); // Auto-resize textarea textareaRef.current.style.height = "auto"; textareaRef.current.style.height = textareaRef.current.scrollHeight + "px"; } }, [isEditing]); const startEditing = () => { setIsEditing(true); onEditingChange?.(true); }; const stopEditing = () => { setIsEditing(false); onEditingChange?.(false); }; const handleSave = () => { // Don't auto-save if there's a pending remote update if (hasPendingRemoteUpdate) return; if (onUpdate && editText.trim() !== text) { onUpdate(editText.trim()); } stopEditing(); }; const handleOverwrite = () => { if (onUpdate && editText.trim() !== text) { onUpdate(editText.trim()); } onOverwrite?.(); stopEditing(); }; const handleCancel = () => { setEditText(text); stopEditing(); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Escape") { handleCancel(); } // Ctrl/Cmd + Enter to save - disabled if there's a pending remote update if (e.key === "Enter" && (e.ctrlKey || e.metaKey) && !hasPendingRemoteUpdate) { handleSave(); } }; const handleBlur = () => { // Don't auto-save on blur if there's a pending remote update if (!hasPendingRemoteUpdate) { handleSave(); } }; const handleInput = (e: React.ChangeEvent) => { setEditText(e.target.value); // Auto-resize e.target.style.height = "auto"; e.target.style.height = e.target.scrollHeight + "px"; }; if (isEditing && onUpdate) { return (