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 || ' '}
);
}
const lang = visibleSection.language;
if (lang === 'python') return renderSyntaxBlock('python');
if (lang === 'json') return renderSyntaxBlock('json');
if (lang === 'markdown') {
return (
{displayContent}
);
}
return (
{displayContent}
);
};
return (
{/* ── Header ─────────────────────────────────────────────── */}
{panelData ? (
<>
{panelData.title}
{hasBothViews && (
}
label="Script"
isActive={panelView === 'script'}
onClick={setPanelView}
/>
}
label="Result"
isActive={panelView === 'output'}
onClick={setPanelView}
/>
)}
>
) : (
Code Panel
)}
{activeSection?.content && (
{copied ? : }
)}
{isEditableScript && !isEditing && (
}
onClick={handleStartEdit}
sx={{
textTransform: 'none',
color: 'var(--muted-text)',
fontSize: '0.75rem',
py: 0.5,
'&:hover': { color: 'var(--accent-yellow)', bgcolor: 'var(--hover-bg)' },
}}
>
Edit
)}
{isEditing && (
<>
}
onClick={handleCancelEdit}
sx={{
textTransform: 'none',
color: 'var(--muted-text)',
fontSize: '0.75rem',
py: 0.5,
'&:hover': { color: 'var(--accent-red)', bgcolor: 'var(--hover-bg)' },
}}
>
Cancel
>
)}
setRightPanelOpen(false)} sx={{ color: 'var(--muted-text)' }}>
{/* ── Main content area ─────────────────────────────────── */}
{!panelData ? (
NO DATA LOADED
) : (
{/* Input / Output toggle */}
{panelData?.input && panelView === 'output' && (
{['input', 'output'].map((tab) => (
setShowInput(tab === 'input')}
variant="caption"
sx={{
fontSize: '0.65rem',
fontWeight: 600,
textTransform: 'uppercase',
letterSpacing: '0.05em',
cursor: 'pointer',
px: 1,
py: 0.25,
borderRadius: 0.5,
color: (tab === 'input') === showInput ? 'var(--text)' : 'var(--muted-text)',
bgcolor: (tab === 'input') === showInput ? 'var(--hover-bg)' : 'transparent',
transition: 'all 0.12s ease',
'&:hover': { color: 'var(--text)' },
}}
>
{tab}
))}
)}
{renderContent()}
)}
{/* ── Plan display (bottom) ─────────────────────────────── */}
{plan && plan.length > 0 && (
CURRENT PLAN
{plan.map((item) => (
{item.content}
))}
)}
);
}