import { useRef, useEffect, useMemo, useState, useCallback } from 'react'; import { Box, Stack, Typography, IconButton, Button, Tooltip } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline'; import CodeIcon from '@mui/icons-material/Code'; import ArticleIcon from '@mui/icons-material/Article'; import EditIcon from '@mui/icons-material/Edit'; import UndoIcon from '@mui/icons-material/Undo'; import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import CheckIcon from '@mui/icons-material/Check'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { vscDarkPlus, vs } from 'react-syntax-highlighter/dist/esm/styles/prism'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useAgentStore } from '@/store/agentStore'; import { useLayoutStore } from '@/store/layoutStore'; import { processLogs } from '@/utils/logProcessor'; import type { PanelView } from '@/store/agentStore'; // ── Helpers ────────────────────────────────────────────────────── function PlanStatusIcon({ status }: { status: string }) { if (status === 'completed') return ; if (status === 'in_progress') return ; return ; } // ── Markdown styles (adapts via CSS vars) ──────────────────────── const markdownSx = { color: 'var(--text)', fontSize: '13px', lineHeight: 1.6, '& p': { m: 0, mb: 1.5, '&:last-child': { mb: 0 } }, '& pre': { bgcolor: 'var(--code-bg)', p: 1.5, borderRadius: 1, overflow: 'auto', fontSize: '12px', border: '1px solid var(--tool-border)', }, '& code': { bgcolor: 'var(--hover-bg)', px: 0.5, py: 0.25, borderRadius: 0.5, fontSize: '12px', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace', }, '& pre code': { bgcolor: 'transparent', p: 0 }, '& a': { color: 'var(--accent-yellow)', textDecoration: 'none', '&:hover': { textDecoration: 'underline' }, }, '& ul, & ol': { pl: 2.5, my: 1 }, '& li': { mb: 0.5 }, '& table': { borderCollapse: 'collapse', width: '100%', my: 2, fontSize: '12px', fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace', }, '& th': { borderBottom: '2px solid var(--border-hover)', textAlign: 'left', p: 1, fontWeight: 600, }, '& td': { borderBottom: '1px solid var(--tool-border)', p: 1, }, '& h1, & h2, & h3, & h4': { mt: 2, mb: 1, fontWeight: 600 }, '& h1': { fontSize: '1.25rem' }, '& h2': { fontSize: '1.1rem' }, '& h3': { fontSize: '1rem' }, '& blockquote': { borderLeft: '3px solid var(--accent-yellow)', pl: 2, ml: 0, color: 'var(--muted-text)', }, } as const; // ── View toggle button ────────────────────────────────────────── function ViewToggle({ view, icon, label, isActive, onClick }: { view: PanelView; icon: React.ReactNode; label: string; isActive: boolean; onClick: (v: PanelView) => void; }) { return ( onClick(view)} sx={{ display: 'flex', alignItems: 'center', gap: 0.5, px: 1.5, py: 0.75, borderRadius: 1, cursor: 'pointer', fontSize: '0.7rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em', whiteSpace: 'nowrap', color: isActive ? 'var(--text)' : 'var(--muted-text)', bgcolor: isActive ? 'var(--tab-active-bg)' : 'transparent', border: '1px solid', borderColor: isActive ? 'var(--tab-active-border)' : 'transparent', transition: 'all 0.15s ease', '&:hover': { bgcolor: 'var(--tab-hover-bg)' }, }} > {icon} {label} ); } // ── Component ──────────────────────────────────────────────────── export default function CodePanel() { const { panelData, panelView, panelEditable, setPanelView, updatePanelScript, setEditedScript, plan } = useAgentStore(); const { setRightPanelOpen, themeMode } = useLayoutStore(); const scrollRef = useRef(null); const textareaRef = useRef(null); const [isEditing, setIsEditing] = useState(false); const [editedContent, setEditedContent] = useState(''); const [originalContent, setOriginalContent] = useState(''); const [copied, setCopied] = useState(false); const [showInput, setShowInput] = useState(false); const isDark = themeMode === 'dark'; const syntaxTheme = isDark ? vscDarkPlus : vs; const activeSection = panelView === 'script' ? panelData?.script : panelData?.output; const hasScript = !!panelData?.script; const hasOutput = !!panelData?.output; const hasBothViews = hasScript && hasOutput; const isEditableScript = panelView === 'script' && panelEditable; const hasUnsavedChanges = isEditing && editedContent !== originalContent; // Reset input toggle when panel data changes useEffect(() => { setShowInput(false); }, [panelData]); // Sync edited content when panel data changes useEffect(() => { if (panelData?.script?.content && panelView === 'script' && panelEditable) { setOriginalContent(panelData.script.content); if (!isEditing) { setEditedContent(panelData.script.content); } } }, [panelData?.script?.content, panelView, panelEditable, isEditing]); // Exit editing when switching away from script view or losing editable useEffect(() => { if (!isEditableScript && isEditing) { setIsEditing(false); } }, [isEditableScript, isEditing]); const handleStartEdit = useCallback(() => { if (panelData?.script?.content) { setEditedContent(panelData.script.content); setOriginalContent(panelData.script.content); setIsEditing(true); setTimeout(() => textareaRef.current?.focus(), 0); } }, [panelData?.script?.content]); const handleCancelEdit = useCallback(() => { setEditedContent(originalContent); setIsEditing(false); }, [originalContent]); const handleSaveEdit = useCallback(() => { if (editedContent !== originalContent) { updatePanelScript(editedContent); const toolCallId = panelData?.parameters?.tool_call_id as string | undefined; if (toolCallId) { setEditedScript(toolCallId, editedContent); } setOriginalContent(editedContent); } setIsEditing(false); }, [panelData?.parameters?.tool_call_id, editedContent, originalContent, updatePanelScript, setEditedScript]); const handleCopy = useCallback(async () => { const contentToCopy = isEditing ? editedContent : (activeSection?.content || ''); if (contentToCopy) { try { await navigator.clipboard.writeText(contentToCopy); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Failed to copy:', err); } } }, [isEditing, editedContent, activeSection?.content]); const visibleSection = (showInput && panelData?.input) ? panelData.input : activeSection; const displayContent = useMemo(() => { if (!visibleSection?.content) return ''; if (!visibleSection.language || visibleSection.language === 'text') { return processLogs(visibleSection.content); } return visibleSection.content; }, [visibleSection?.content, visibleSection?.language]); // Auto-scroll only for live log streaming, not when opening panel const hasAutoScrolled = useRef(false); useEffect(() => { hasAutoScrolled.current = false; }, [panelData]); useEffect(() => { if (scrollRef.current && panelView === 'output' && hasAutoScrolled.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } hasAutoScrolled.current = true; }, [displayContent, panelView]); // ── Syntax-highlighted code block (DRY) ──────────────────────── const renderSyntaxBlock = (language: string) => ( {displayContent} ); // ── Content renderer ─────────────────────────────────────────── const renderContent = () => { if (!visibleSection?.content) { return ( NO CONTENT TO DISPLAY ); } if (!showInput && isEditing && isEditableScript) { return ( {editedContent || ' '}