| import { useGifGenerator } from '@/hooks/useGifGenerator';
|
| import { useJsonExporter } from '@/hooks/useJsonExporter';
|
| import { selectError, selectFinalStep, selectSteps, selectTrace, useAgentStore } from '@/stores/agentStore';
|
| import { AgentStep, AgentTraceMetadata } from '@/types/agent';
|
| import ImageIcon from '@mui/icons-material/Image';
|
| import MonitorIcon from '@mui/icons-material/Monitor';
|
| import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
| import { Box, Button, CircularProgress, keyframes, Typography } from '@mui/material';
|
| import React from 'react';
|
| import { useNavigate } from 'react-router-dom';
|
| import { CompletionView } from './completionview/CompletionView';
|
|
|
|
|
| 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;
|
| isRunning?: boolean;
|
| }
|
|
|
| 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);
|
|
|
|
|
| const latestScreenshot = steps && steps.length > 0 ? steps[steps.length - 1].image : null;
|
|
|
|
|
| const { isGenerating, error: gifError, generateAndDownloadGif } = useGifGenerator({
|
| steps: steps || [],
|
| traceId: finalStep?.metadata.traceId || '',
|
| });
|
|
|
|
|
| const { downloadTraceAsJson } = useJsonExporter({
|
| trace,
|
| steps: steps || [],
|
| metadata: finalStep?.metadata || metadata,
|
| finalStep,
|
| });
|
|
|
|
|
| const getFinalAnswer = (): string | null => {
|
| console.log('🔍 getFinalAnswer - steps:', steps);
|
| if (!steps || steps.length === 0) {
|
| console.log('❌ No steps available');
|
| return null;
|
| }
|
|
|
|
|
| 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) {
|
|
|
| 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...');
|
|
|
|
|
| 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);
|
|
|
|
|
| const showStatus = !isRunning && !selectedStep && finalStep;
|
|
|
|
|
| const handleBackToHome = () => {
|
|
|
| useAgentStore.getState().resetAgent();
|
|
|
|
|
| window.location.href = '/';
|
| };
|
|
|
|
|
| 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' || finalStep?.type === 'sandbox_timeout') ? '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={<PlayCircleIcon 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 ? (
|
|
|
| <CompletionView
|
| finalStep={finalStep}
|
| trace={trace}
|
| steps={steps}
|
| metadata={metadata}
|
| finalAnswer={finalAnswer}
|
| isGenerating={isGenerating}
|
| gifError={gifError}
|
| onGenerateGif={generateAndDownloadGif}
|
| onDownloadJson={downloadTraceAsJson}
|
| onBackToHome={handleBackToHome}
|
| />
|
| ) : selectedStep ? (
|
|
|
| <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 ? (
|
|
|
| <iframe
|
| src={vncUrl}
|
| style={{ width: '100%', height: '100%', border: 'none' }}
|
| title="OS Stream"
|
| lang="en"
|
| />
|
| ) : latestScreenshot ? (
|
|
|
| <Box
|
| sx={{
|
| width: '100%',
|
| height: '100%',
|
| display: 'flex',
|
| alignItems: 'center',
|
| justifyContent: 'center',
|
| overflow: 'auto',
|
| backgroundColor: 'black',
|
| position: 'relative',
|
| }}
|
| >
|
| <img
|
| src={latestScreenshot}
|
| alt="Latest screenshot"
|
| style={{
|
| maxWidth: '100%',
|
| maxHeight: '100%',
|
| objectFit: 'contain',
|
| }}
|
| />
|
| </Box>
|
| ) : isAgentProcessing ? (
|
|
|
| <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' }}>
|
| Starting FARA Agent...
|
| </Typography>
|
| <Typography variant="caption" sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
|
| Initializing browser environment
|
| </Typography>
|
| </Box>
|
| ) : (
|
|
|
| <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>
|
| );
|
| };
|
|
|