Spaces:
Running
Running
| import React from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { Box, Typography, CircularProgress, Button, keyframes } from '@mui/material'; | |
| import MonitorIcon from '@mui/icons-material/Monitor'; | |
| import ImageIcon from '@mui/icons-material/Image'; | |
| import PlayArrowIcon from '@mui/icons-material/PlayArrow'; | |
| import { AgentTraceMetadata, AgentStep } from '@/types/agent'; | |
| import { useAgentStore, selectError, selectFinalStep, selectSteps, selectTrace } from '@/stores/agentStore'; | |
| import { CompletionView } from './CompletionView'; | |
| import { useGifGenerator } from '@/hooks/useGifGenerator'; | |
| import { useJsonExporter } from '@/hooks/useJsonExporter'; | |
| // Animation for live indicator | |
| const livePulse = keyframes` | |
| 0%, 100% { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| 50% { | |
| opacity: 0.7; | |
| transform: scale(1.2); | |
| } | |
| `; | |
| interface SandboxViewerProps { | |
| vncUrl: string; | |
| isAgentProcessing?: boolean; | |
| metadata?: AgentTraceMetadata; | |
| traceStartTime?: Date; | |
| selectedStep?: AgentStep | null; // The step to display in time-travel mode | |
| isRunning?: boolean; // Is the agent currently running | |
| } | |
| export const SandboxViewer: React.FC<SandboxViewerProps> = ({ | |
| vncUrl, | |
| isAgentProcessing = false, | |
| metadata, | |
| traceStartTime, | |
| selectedStep, | |
| isRunning = false | |
| }) => { | |
| const navigate = useNavigate(); | |
| const error = useAgentStore(selectError); | |
| const finalStep = useAgentStore(selectFinalStep); | |
| const steps = useAgentStore(selectSteps); | |
| const trace = useAgentStore(selectTrace); | |
| const resetAgent = useAgentStore((state) => state.resetAgent); | |
| const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); | |
| // Hook to generate GIF | |
| const { isGenerating, error: gifError, generateAndDownloadGif } = useGifGenerator({ | |
| steps: steps || [], | |
| traceId: finalStep?.metadata.traceId || '', | |
| }); | |
| // Hook to export JSON | |
| const { downloadTraceAsJson } = useJsonExporter({ | |
| trace, | |
| steps: steps || [], | |
| metadata: finalStep?.metadata || metadata, | |
| }); | |
| // Extract final_answer from the last step, or fallback to last thought | |
| const getFinalAnswer = (): string | null => { | |
| console.log('🔍 getFinalAnswer - steps:', steps); | |
| if (!steps || steps.length === 0) { | |
| console.log('❌ No steps available'); | |
| return null; | |
| } | |
| // Try to find final_answer in any step (iterate backwards) | |
| for (let i = steps.length - 1; i >= 0; i--) { | |
| const step = steps[i]; | |
| if (step.actions && Array.isArray(step.actions)) { | |
| const finalAnswerAction = step.actions.find( | |
| (action) => action.function_name === 'final_answer' | |
| ); | |
| if (finalAnswerAction) { | |
| // Handle both named parameter and positional argument | |
| const result = finalAnswerAction?.parameters?.answer || finalAnswerAction?.parameters?.arg_0 || null; | |
| console.log('✅ Final answer found in step', i + 1, ':', result); | |
| return result; | |
| } | |
| } | |
| } | |
| console.log('🔍 No final_answer found, looking for last thought...'); | |
| // Fallback: find the last step with a thought (iterate backwards) | |
| for (let i = steps.length - 1; i >= 0; i--) { | |
| const step = steps[i]; | |
| if (step.thought) { | |
| console.log('📝 Using thought from step', i + 1, 'as fallback:', step.thought); | |
| return step.thought; | |
| } | |
| } | |
| console.log('❌ No final answer or thought found in any step'); | |
| return null; | |
| }; | |
| const finalAnswer = getFinalAnswer(); | |
| console.log('🎯 Final answer to display:', finalAnswer); | |
| // Determine if we should show success/fail status | |
| const showStatus = !isRunning && !selectedStep && finalStep; | |
| // Handler to go back to home | |
| const handleBackToHome = () => { | |
| resetAgent(); | |
| navigate('/'); | |
| }; | |
| // Handler to go back to live mode | |
| const handleGoLive = () => { | |
| setSelectedStepIndex(null); | |
| }; | |
| return ( | |
| <Box | |
| sx={{ | |
| flex: '1 1 auto', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| position: 'relative', | |
| border: '1px solid', | |
| borderColor: showStatus | |
| ? (finalStep?.type === 'failure' ? 'error.main' : 'success.main') | |
| : ((vncUrl || isAgentProcessing) && !selectedStep && !showStatus ? 'primary.main' : 'divider'), | |
| borderRadius: '12px', | |
| backgroundColor: 'background.paper', | |
| transition: 'border 0.3s ease', | |
| overflow: 'hidden', | |
| }} | |
| > | |
| {/* Live Badge or Go Live Button */} | |
| {vncUrl && !showStatus && ( | |
| <> | |
| {!selectedStep ? ( | |
| // Live Badge when in live mode | |
| <Box | |
| sx={{ | |
| position: 'absolute', | |
| top: 12, | |
| right: 12, | |
| zIndex: 10, | |
| display: 'flex', | |
| alignItems: 'center', | |
| gap: 1, | |
| px: 2, | |
| py: 1, | |
| backgroundColor: (theme) => | |
| theme.palette.mode === 'dark' | |
| ? 'rgba(0, 0, 0, 0.7)' | |
| : 'rgba(255, 255, 255, 0.9)', | |
| backdropFilter: 'blur(8px)', | |
| borderRadius: 0.75, | |
| border: '1px solid', | |
| borderColor: 'primary.main', | |
| boxShadow: (theme) => | |
| theme.palette.mode === 'dark' | |
| ? '0 2px 8px rgba(0, 0, 0, 0.4)' | |
| : '0 2px 8px rgba(0, 0, 0, 0.1)', | |
| }} | |
| > | |
| <Box | |
| sx={{ | |
| width: 10, | |
| height: 10, | |
| borderRadius: '50%', | |
| backgroundColor: 'error.main', | |
| animation: `${livePulse} 2s ease-in-out infinite`, | |
| }} | |
| /> | |
| <Typography | |
| variant="caption" | |
| sx={{ | |
| fontSize: '0.8rem', | |
| fontWeight: 700, | |
| color: 'text.primary', | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.5px', | |
| }} | |
| > | |
| Live | |
| </Typography> | |
| </Box> | |
| ) : ( | |
| // Go Live Button when viewing a specific step | |
| <Button | |
| onClick={handleGoLive} | |
| startIcon={<PlayArrowIcon sx={{ fontSize: 20 }} />} | |
| sx={{ | |
| position: 'absolute', | |
| top: 12, | |
| right: 12, | |
| zIndex: 10, | |
| px: 2, | |
| py: 1, | |
| backgroundColor: (theme) => | |
| theme.palette.mode === 'dark' | |
| ? 'rgba(0, 0, 0, 0.7)' | |
| : 'rgba(255, 255, 255, 0.9)', | |
| backdropFilter: 'blur(8px)', | |
| borderRadius: 0.75, | |
| border: '1px solid', | |
| borderColor: 'primary.main', | |
| boxShadow: (theme) => | |
| theme.palette.mode === 'dark' | |
| ? '0 2px 8px rgba(0, 0, 0, 0.4)' | |
| : '0 2px 8px rgba(0, 0, 0, 0.1)', | |
| fontSize: '0.8rem', | |
| fontWeight: 700, | |
| textTransform: 'uppercase', | |
| letterSpacing: '0.5px', | |
| color: 'primary.main', | |
| '&:hover': { | |
| backgroundColor: (theme) => | |
| theme.palette.mode === 'dark' | |
| ? 'rgba(0, 0, 0, 0.85)' | |
| : 'rgba(255, 255, 255, 1)', | |
| borderColor: 'primary.dark', | |
| }, | |
| }} | |
| > | |
| Go Live | |
| </Button> | |
| )} | |
| </> | |
| )} | |
| <Box | |
| sx={{ | |
| flex: 1, | |
| minHeight: 0, | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }} | |
| > | |
| {showStatus && finalStep ? ( | |
| // Show success/fail status when agent has completed | |
| <CompletionView | |
| finalStep={finalStep} | |
| trace={trace} | |
| steps={steps} | |
| finalAnswer={finalAnswer} | |
| isGenerating={isGenerating} | |
| gifError={gifError} | |
| onGenerateGif={generateAndDownloadGif} | |
| onDownloadJson={downloadTraceAsJson} | |
| onBackToHome={handleBackToHome} | |
| /> | |
| ) : selectedStep ? ( | |
| // Time-travel mode: Show screenshot of selected step | |
| <Box | |
| sx={{ | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| overflow: 'auto', | |
| backgroundColor: 'black', | |
| position: 'relative', | |
| }} | |
| > | |
| {selectedStep.image ? ( | |
| <img | |
| src={selectedStep.image} | |
| alt="Step screenshot" | |
| style={{ | |
| maxWidth: '100%', | |
| maxHeight: '100%', | |
| objectFit: 'contain', | |
| }} | |
| /> | |
| ) : ( | |
| <Box | |
| sx={{ | |
| textAlign: 'center', | |
| p: 4, | |
| color: 'text.secondary', | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }} | |
| > | |
| <ImageIcon sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} /> | |
| <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5, fontSize: '0.875rem', color: 'text.primary' }}> | |
| No screenshot available | |
| </Typography> | |
| <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}> | |
| This step doesn't have a screenshot | |
| </Typography> | |
| </Box> | |
| )} | |
| </Box> | |
| ) : vncUrl ? ( | |
| // Live mode: Show VNC stream | |
| <iframe | |
| src={vncUrl} | |
| style={{ width: '100%', height: '100%', border: 'none' }} | |
| title="OS Stream" | |
| /> | |
| ) : isAgentProcessing ? ( | |
| // Loading state | |
| <Box | |
| sx={{ | |
| textAlign: 'center', | |
| p: 4, | |
| color: 'text.secondary', | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }} | |
| > | |
| <CircularProgress | |
| size={48} | |
| sx={{ | |
| mb: 2, | |
| color: 'primary.main' | |
| }} | |
| /> | |
| <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5, fontSize: '0.875rem', color: 'text.primary' }}> | |
| Connecting to E2B... | |
| </Typography> | |
| <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}> | |
| Setting up sandbox environment | |
| </Typography> | |
| </Box> | |
| ) : ( | |
| // No stream available | |
| <Box | |
| sx={{ | |
| textAlign: 'center', | |
| p: 4, | |
| color: 'text.secondary', | |
| width: '100%', | |
| height: '100%', | |
| display: 'flex', | |
| flexDirection: 'column', | |
| alignItems: 'center', | |
| justifyContent: 'center', | |
| }} | |
| > | |
| <MonitorIcon sx={{ fontSize: 48, mb: 2, opacity: 0.5 }} /> | |
| <Typography variant="body2" sx={{ fontWeight: 600, mb: 0.5, fontSize: '0.875rem' }}> | |
| No stream available | |
| </Typography> | |
| <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}> | |
| Stream will appear when agent starts | |
| </Typography> | |
| </Box> | |
| )} | |
| </Box> | |
| </Box> | |
| ); | |
| }; | |