Spaces:
Running
Running
| import { useRef, useEffect, useMemo, useState, useCallback } from 'react'; | |
| import { Box, 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 TerminalIcon from '@mui/icons-material/Terminal'; | |
| 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 } 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 JobStatusHeader from './JobStatusHeader'; | |
| export default function CodePanel() { | |
| const { panelContent, panelTabs, activePanelTab, setActivePanelTab, removePanelTab, plan, updatePanelTabContent, setEditedScript, activeJob } = useAgentStore(); | |
| const { setRightPanelOpen } = useLayoutStore(); | |
| const scrollRef = useRef<HTMLDivElement>(null); | |
| const textareaRef = useRef<HTMLTextAreaElement>(null); | |
| const [isEditing, setIsEditing] = useState(false); | |
| const [editedContent, setEditedContent] = useState(''); | |
| const [originalContent, setOriginalContent] = useState(''); | |
| const [copied, setCopied] = useState(false); | |
| // Get the active tab content, or fall back to panelContent for backwards compatibility | |
| const activeTab = panelTabs.find(t => t.id === activePanelTab); | |
| const currentContent = activeTab || panelContent; | |
| // Check if this is an editable script tab | |
| const isEditableScript = activeTab?.id === 'script' && activeTab?.language === 'python'; | |
| const hasUnsavedChanges = isEditing && editedContent !== originalContent; | |
| // Sync edited content when switching tabs or content changes | |
| useEffect(() => { | |
| if (currentContent?.content && isEditableScript) { | |
| setOriginalContent(currentContent.content); | |
| if (!isEditing) { | |
| setEditedContent(currentContent.content); | |
| } | |
| } | |
| }, [currentContent?.content, isEditableScript, isEditing]); | |
| const handleStartEdit = useCallback(() => { | |
| if (currentContent?.content) { | |
| setEditedContent(currentContent.content); | |
| setOriginalContent(currentContent.content); | |
| setIsEditing(true); | |
| // Focus textarea after render | |
| setTimeout(() => textareaRef.current?.focus(), 0); | |
| } | |
| }, [currentContent?.content]); | |
| const handleCancelEdit = useCallback(() => { | |
| setEditedContent(originalContent); | |
| setIsEditing(false); | |
| }, [originalContent]); | |
| const handleSaveEdit = useCallback(() => { | |
| if (activeTab && editedContent !== originalContent) { | |
| // Update the panel tab content | |
| updatePanelTabContent(activeTab.id, editedContent); | |
| // Store the edited script for approval - use tool_call_id from parameters | |
| const toolCallId = activeTab.parameters?.tool_call_id; | |
| if (toolCallId) { | |
| setEditedScript(toolCallId, editedContent); | |
| } | |
| setOriginalContent(editedContent); | |
| } | |
| setIsEditing(false); | |
| }, [activeTab, editedContent, originalContent, updatePanelTabContent, setEditedScript]); | |
| const handleCopy = useCallback(async () => { | |
| const contentToCopy = isEditing ? editedContent : (currentContent?.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, currentContent?.content]); | |
| const displayContent = useMemo(() => { | |
| if (!currentContent?.content) return ''; | |
| // Apply log processing only for text/logs, not for code/json | |
| if (!currentContent.language || currentContent.language === 'text') { | |
| return processLogs(currentContent.content); | |
| } | |
| return currentContent.content; | |
| }, [currentContent?.content, currentContent?.language]); | |
| useEffect(() => { | |
| // Auto-scroll only for logs tab | |
| if (scrollRef.current && activePanelTab === 'logs') { | |
| scrollRef.current.scrollTop = scrollRef.current.scrollHeight; | |
| } | |
| }, [displayContent, activePanelTab]); | |
| const hasTabs = panelTabs.length > 0; | |
| return ( | |
| <Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', bgcolor: 'var(--panel)' }}> | |
| {/* Header - Fixed 60px to align */} | |
| <Box sx={{ | |
| height: '60px', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'space-between', | |
| px: 2, | |
| borderBottom: '1px solid rgba(255,255,255,0.03)' | |
| }}> | |
| {hasTabs ? ( | |
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}> | |
| {panelTabs.map((tab) => { | |
| const isActive = activePanelTab === tab.id; | |
| // Choose icon based on tab type | |
| let icon = <TerminalIcon sx={{ fontSize: 14 }} />; | |
| if (tab.id === 'script' || tab.language === 'python') { | |
| icon = <CodeIcon sx={{ fontSize: 14 }} />; | |
| } else if (tab.id === 'tool_output' || tab.language === 'markdown' || tab.language === 'json') { | |
| icon = <ArticleIcon sx={{ fontSize: 14 }} />; | |
| } | |
| return ( | |
| <Box | |
| key={tab.id} | |
| onClick={() => setActivePanelTab(tab.id)} | |
| 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', | |
| color: isActive ? 'var(--text)' : 'var(--muted-text)', | |
| bgcolor: isActive ? 'rgba(255,255,255,0.08)' : 'transparent', | |
| border: '1px solid', | |
| borderColor: isActive ? 'rgba(255,255,255,0.1)' : 'transparent', | |
| transition: 'all 0.15s ease', | |
| '&:hover': { | |
| bgcolor: 'rgba(255,255,255,0.05)', | |
| }, | |
| }} | |
| > | |
| {icon} | |
| <span>{tab.title}</span> | |
| <Box | |
| component="span" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| removePanelTab(tab.id); | |
| }} | |
| sx={{ | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| ml: 0.5, | |
| width: 16, | |
| height: 16, | |
| borderRadius: '50%', | |
| fontSize: '0.65rem', | |
| opacity: 0.5, | |
| '&:hover': { | |
| opacity: 1, | |
| bgcolor: 'rgba(255,255,255,0.1)', | |
| }, | |
| }} | |
| > | |
| ✕ | |
| </Box> | |
| </Box> | |
| ); | |
| })} | |
| </Box> | |
| ) : ( | |
| <Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}> | |
| {currentContent?.title || 'Code Panel'} | |
| </Typography> | |
| )} | |
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> | |
| {/* Copy button */} | |
| {currentContent?.content && ( | |
| <Tooltip title={copied ? 'Copied!' : 'Copy'} placement="top"> | |
| <IconButton | |
| size="small" | |
| onClick={handleCopy} | |
| sx={{ | |
| color: copied ? 'var(--accent-green)' : 'var(--muted-text)', | |
| '&:hover': { | |
| color: 'var(--accent-primary)', | |
| bgcolor: 'rgba(255,255,255,0.05)', | |
| }, | |
| }} | |
| > | |
| {copied ? <CheckIcon sx={{ fontSize: 18 }} /> : <ContentCopyIcon sx={{ fontSize: 18 }} />} | |
| </IconButton> | |
| </Tooltip> | |
| )} | |
| {/* Edit controls for script tab */} | |
| {isEditableScript && !isEditing && ( | |
| <Button | |
| size="small" | |
| startIcon={<EditIcon sx={{ fontSize: 14 }} />} | |
| onClick={handleStartEdit} | |
| sx={{ | |
| textTransform: 'none', | |
| color: 'var(--muted-text)', | |
| fontSize: '0.75rem', | |
| py: 0.5, | |
| '&:hover': { | |
| color: 'var(--accent-primary)', | |
| bgcolor: 'rgba(255,255,255,0.05)', | |
| }, | |
| }} | |
| > | |
| Edit | |
| </Button> | |
| )} | |
| {isEditing && ( | |
| <> | |
| <Button | |
| size="small" | |
| startIcon={<UndoIcon sx={{ fontSize: 14 }} />} | |
| onClick={handleCancelEdit} | |
| sx={{ | |
| textTransform: 'none', | |
| color: 'var(--muted-text)', | |
| fontSize: '0.75rem', | |
| py: 0.5, | |
| '&:hover': { | |
| color: 'var(--accent-red)', | |
| bgcolor: 'rgba(255,255,255,0.05)', | |
| }, | |
| }} | |
| > | |
| Cancel | |
| </Button> | |
| <Button | |
| size="small" | |
| variant="contained" | |
| onClick={handleSaveEdit} | |
| disabled={!hasUnsavedChanges} | |
| sx={{ | |
| textTransform: 'none', | |
| fontSize: '0.75rem', | |
| py: 0.5, | |
| bgcolor: hasUnsavedChanges ? 'var(--accent-green)' : 'rgba(255,255,255,0.1)', | |
| color: hasUnsavedChanges ? '#000' : 'var(--muted-text)', | |
| '&:hover': { | |
| bgcolor: hasUnsavedChanges ? 'var(--accent-green)' : 'rgba(255,255,255,0.1)', | |
| opacity: 0.9, | |
| }, | |
| '&.Mui-disabled': { | |
| bgcolor: 'rgba(255,255,255,0.05)', | |
| color: 'rgba(255,255,255,0.3)', | |
| }, | |
| }} | |
| > | |
| Save | |
| </Button> | |
| </> | |
| )} | |
| <IconButton size="small" onClick={() => setRightPanelOpen(false)} sx={{ color: 'var(--muted-text)' }}> | |
| <CloseIcon fontSize="small" /> | |
| </IconButton> | |
| </Box> | |
| </Box> | |
| {/* Job Status Header - shown when there's an active job */} | |
| {activeJob && (activePanelTab === 'logs' || activePanelTab === 'script') && ( | |
| <JobStatusHeader job={activeJob} /> | |
| )} | |
| {/* Main Content Area */} | |
| <Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}> | |
| {!currentContent ? ( | |
| <Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', p: 4 }}> | |
| <Typography variant="body2" color="text.secondary" sx={{ opacity: 0.5 }}> | |
| NO DATA LOADED | |
| </Typography> | |
| </Box> | |
| ) : ( | |
| <Box sx={{ flex: 1, overflow: 'hidden', p: 2 }}> | |
| {activeTab?.id === 'tool_output' && ( | |
| <Typography | |
| variant="caption" | |
| sx={{ | |
| display: 'block', | |
| mb: 1, | |
| color: 'var(--muted-text)', | |
| fontSize: '0.7rem', | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.05em', | |
| }} | |
| > | |
| Tool output | |
| </Typography> | |
| )} | |
| <Box | |
| ref={scrollRef} | |
| className="code-panel" | |
| sx={{ | |
| background: '#0A0B0C', | |
| borderRadius: 'var(--radius-md)', | |
| padding: '18px', | |
| border: '1px solid rgba(255,255,255,0.03)', | |
| fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace', | |
| fontSize: '13px', | |
| lineHeight: 1.55, | |
| height: '100%', | |
| overflow: 'auto', | |
| }} | |
| > | |
| {currentContent.content ? ( | |
| isEditing && isEditableScript ? ( | |
| <textarea | |
| ref={textareaRef} | |
| value={editedContent} | |
| onChange={(e) => setEditedContent(e.target.value)} | |
| spellCheck={false} | |
| style={{ | |
| width: '100%', | |
| height: '100%', | |
| background: 'transparent', | |
| border: 'none', | |
| outline: 'none', | |
| resize: 'none', | |
| color: 'var(--text)', | |
| fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace', | |
| fontSize: '13px', | |
| lineHeight: 1.55, | |
| }} | |
| /> | |
| ) : currentContent.language === 'python' ? ( | |
| <SyntaxHighlighter | |
| language="python" | |
| style={vscDarkPlus} | |
| customStyle={{ | |
| margin: 0, | |
| padding: 0, | |
| background: 'transparent', | |
| fontSize: '13px', | |
| fontFamily: 'inherit', | |
| }} | |
| wrapLines={true} | |
| wrapLongLines={true} | |
| > | |
| {displayContent} | |
| </SyntaxHighlighter> | |
| ) : currentContent.language === 'json' ? ( | |
| <SyntaxHighlighter | |
| language="json" | |
| style={vscDarkPlus} | |
| customStyle={{ | |
| margin: 0, | |
| padding: 0, | |
| background: 'transparent', | |
| fontSize: '13px', | |
| fontFamily: 'inherit', | |
| }} | |
| wrapLines={true} | |
| wrapLongLines={true} | |
| > | |
| {displayContent} | |
| </SyntaxHighlighter> | |
| ) : currentContent.language === 'markdown' ? ( | |
| <Box sx={{ | |
| color: 'var(--text)', | |
| fontSize: '13px', | |
| lineHeight: 1.6, | |
| '& p': { m: 0, mb: 1.5, '&:last-child': { mb: 0 } }, | |
| '& pre': { | |
| bgcolor: 'rgba(0,0,0,0.4)', | |
| p: 1.5, | |
| borderRadius: 1, | |
| overflow: 'auto', | |
| fontSize: '12px', | |
| border: '1px solid rgba(255,255,255,0.05)', | |
| }, | |
| '& code': { | |
| bgcolor: 'rgba(255,255,255,0.05)', | |
| 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 rgba(255,255,255,0.15)', | |
| textAlign: 'left', | |
| p: 1, | |
| fontWeight: 600, | |
| }, | |
| '& td': { | |
| borderBottom: '1px solid rgba(255,255,255,0.05)', | |
| 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 rgba(255,255,255,0.2)', | |
| pl: 2, | |
| ml: 0, | |
| color: 'var(--muted-text)', | |
| }, | |
| }}> | |
| <ReactMarkdown remarkPlugins={[remarkGfm]}>{displayContent}</ReactMarkdown> | |
| </Box> | |
| ) : ( | |
| <Box component="pre" sx={{ | |
| m: 0, | |
| fontFamily: 'inherit', | |
| color: 'var(--text)', | |
| whiteSpace: 'pre-wrap', | |
| wordBreak: 'break-all' | |
| }}> | |
| <code>{displayContent}</code> | |
| </Box> | |
| ) | |
| ) : ( | |
| <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', opacity: 0.5 }}> | |
| <Typography variant="caption"> | |
| NO CONTENT TO DISPLAY | |
| </Typography> | |
| </Box> | |
| )} | |
| </Box> | |
| </Box> | |
| )} | |
| </Box> | |
| {/* Plan Display at Bottom */} | |
| {plan && plan.length > 0 && ( | |
| <Box sx={{ | |
| borderTop: '1px solid rgba(255,255,255,0.03)', | |
| bgcolor: 'rgba(0,0,0,0.2)', | |
| maxHeight: '30%', | |
| display: 'flex', | |
| flexDirection: 'column' | |
| }}> | |
| <Box sx={{ p: 1.5, borderBottom: '1px solid rgba(255,255,255,0.03)', display: 'flex', alignItems: 'center', gap: 1 }}> | |
| <Typography variant="caption" sx={{ fontWeight: 600, color: 'var(--muted-text)', textTransform: 'uppercase', letterSpacing: '0.05em' }}> | |
| CURRENT PLAN | |
| </Typography> | |
| </Box> | |
| <Box sx={{ p: 2, overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 1 }}> | |
| {plan.map((item) => ( | |
| <Box key={item.id} sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}> | |
| <Box sx={{ mt: 0.2 }}> | |
| {item.status === 'completed' && <CheckCircleIcon sx={{ fontSize: 16, color: 'var(--accent-green)' }} />} | |
| {item.status === 'in_progress' && <PlayCircleOutlineIcon sx={{ fontSize: 16, color: 'var(--accent-yellow)' }} />} | |
| {item.status === 'pending' && <RadioButtonUncheckedIcon sx={{ fontSize: 16, color: 'var(--muted-text)', opacity: 0.5 }} />} | |
| </Box> | |
| <Typography | |
| variant="body2" | |
| sx={{ | |
| fontSize: '13px', | |
| fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, monospace', | |
| color: item.status === 'completed' ? 'var(--muted-text)' : 'var(--text)', | |
| textDecoration: item.status === 'completed' ? 'line-through' : 'none', | |
| opacity: item.status === 'pending' ? 0.7 : 1 | |
| }} | |
| > | |
| {item.content} | |
| </Typography> | |
| </Box> | |
| ))} | |
| </Box> | |
| </Box> | |
| )} | |
| </Box> | |
| ); | |
| } | |