|
|
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>
|
|
|
);
|
|
|
};
|
|
|
|