Spaces:
Running
Running
Front update 4 (#17)
Browse files* add emergency, update stacktrace, update tmeline, update gifgenerator resolution
* update timeline
* update stepcard
* fix: remove trailing whitespaces (pre-commit auto-fix)
* handle new error cases
- cua2-front/src/App.tsx +3 -1
- cua2-front/src/components/Header.tsx +46 -5
- cua2-front/src/components/sandbox/SandboxViewer.tsx +5 -3
- cua2-front/src/components/sandbox/completionview/CompletionView.tsx +51 -14
- cua2-front/src/components/steps/FinalStepCard.tsx +51 -16
- cua2-front/src/components/steps/StepCard.tsx +8 -1
- cua2-front/src/components/steps/StepsList.tsx +48 -41
- cua2-front/src/components/steps/ThinkingStepCard.tsx +11 -5
- cua2-front/src/components/timeline/Timeline.tsx +118 -45
- cua2-front/src/hooks/useAgentWebSocket.ts +17 -2
- cua2-front/src/hooks/useGifGenerator.ts +1 -3
- cua2-front/src/hooks/useJsonExporter.ts +5 -3
- cua2-front/src/services/gifGenerator.ts +65 -24
- cua2-front/src/services/jsonExporter.ts +37 -2
- cua2-front/src/stores/agentStore.ts +56 -36
- cua2-front/src/types/agent.ts +1 -1
cua2-front/src/App.tsx
CHANGED
|
@@ -13,8 +13,10 @@ const App = () => {
|
|
| 13 |
const theme = useMemo(() => getTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode]);
|
| 14 |
|
| 15 |
// Initialize WebSocket connection at app level so it persists across route changes
|
|
|
|
| 16 |
|
| 17 |
-
|
|
|
|
| 18 |
|
| 19 |
return (
|
| 20 |
<ThemeProvider theme={theme}>
|
|
|
|
| 13 |
const theme = useMemo(() => getTheme(isDarkMode ? 'dark' : 'light'), [isDarkMode]);
|
| 14 |
|
| 15 |
// Initialize WebSocket connection at app level so it persists across route changes
|
| 16 |
+
const { stopCurrentTask } = useAgentWebSocket({ url: getWebSocketUrl() });
|
| 17 |
|
| 18 |
+
// Store stopCurrentTask in window for global access
|
| 19 |
+
(window as Window & { __stopCurrentTask?: () => void }).__stopCurrentTask = stopCurrentTask;
|
| 20 |
|
| 21 |
return (
|
| 22 |
<ThemeProvider theme={theme}>
|
cua2-front/src/components/Header.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
-
import { AppBar, Toolbar, Box, Typography, Chip, IconButton, CircularProgress, keyframes } from '@mui/material';
|
| 3 |
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
| 4 |
import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
|
| 5 |
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
|
|
@@ -11,6 +11,7 @@ import OutputIcon from '@mui/icons-material/Output';
|
|
| 11 |
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
| 12 |
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
|
| 13 |
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
|
|
|
| 14 |
import { useAgentStore, selectTrace, selectError, selectIsDarkMode, selectMetadata, selectIsConnectingToE2B, selectFinalStep } from '@/stores/agentStore';
|
| 15 |
|
| 16 |
interface HeaderProps {
|
|
@@ -125,10 +126,16 @@ export const Header: React.FC<HeaderProps> = ({ isAgentProcessing, onBackToHome
|
|
| 125 |
const getTaskStatus = () => {
|
| 126 |
// If we have a final step, use its type
|
| 127 |
if (finalStep) {
|
| 128 |
-
|
| 129 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
-
return { label: 'Completed', color: 'success', icon: <CheckIcon sx={{ fontSize: 16, color: 'success.main' }} /> };
|
| 132 |
}
|
| 133 |
// Otherwise check running states
|
| 134 |
if (isConnectingToE2B) return { label: 'Connecting to E2B...', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> };
|
|
@@ -141,6 +148,14 @@ export const Header: React.FC<HeaderProps> = ({ isAgentProcessing, onBackToHome
|
|
| 141 |
// Extract model name from modelId (e.g., "Qwen/Qwen3-VL-8B-Instruct" -> "Qwen3-VL-8B-Instruct")
|
| 142 |
const modelName = trace?.modelId?.split('/').pop() || 'Unknown Model';
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
return (
|
| 145 |
<AppBar
|
| 146 |
position="static"
|
|
@@ -188,8 +203,34 @@ export const Header: React.FC<HeaderProps> = ({ isAgentProcessing, onBackToHome
|
|
| 188 |
</Typography>
|
| 189 |
</Box>
|
| 190 |
|
| 191 |
-
{/* Right side: Dark Mode */}
|
| 192 |
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
<IconButton
|
| 194 |
onClick={toggleDarkMode}
|
| 195 |
size="small"
|
|
|
|
| 1 |
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { AppBar, Toolbar, Box, Typography, Chip, IconButton, CircularProgress, keyframes, Button } from '@mui/material';
|
| 3 |
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
| 4 |
import LightModeOutlined from '@mui/icons-material/LightModeOutlined';
|
| 5 |
import DarkModeOutlined from '@mui/icons-material/DarkModeOutlined';
|
|
|
|
| 11 |
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
| 12 |
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
|
| 13 |
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
| 14 |
+
import StopCircleIcon from '@mui/icons-material/StopCircle';
|
| 15 |
import { useAgentStore, selectTrace, selectError, selectIsDarkMode, selectMetadata, selectIsConnectingToE2B, selectFinalStep } from '@/stores/agentStore';
|
| 16 |
|
| 17 |
interface HeaderProps {
|
|
|
|
| 126 |
const getTaskStatus = () => {
|
| 127 |
// If we have a final step, use its type
|
| 128 |
if (finalStep) {
|
| 129 |
+
switch (finalStep.type) {
|
| 130 |
+
case 'failure':
|
| 131 |
+
return { label: 'Task failed', color: 'error', icon: <CloseIcon sx={{ fontSize: 16, color: 'error.main' }} /> };
|
| 132 |
+
case 'stopped':
|
| 133 |
+
return { label: 'Task stopped', color: 'warning', icon: <StopCircleIcon sx={{ fontSize: 16, color: 'warning.main' }} /> };
|
| 134 |
+
case 'max_steps_reached':
|
| 135 |
+
return { label: 'Max steps reached', color: 'warning', icon: <HourglassEmptyIcon sx={{ fontSize: 16, color: 'warning.main' }} /> };
|
| 136 |
+
case 'success':
|
| 137 |
+
return { label: 'Completed', color: 'success', icon: <CheckIcon sx={{ fontSize: 16, color: 'success.main' }} /> };
|
| 138 |
}
|
|
|
|
| 139 |
}
|
| 140 |
// Otherwise check running states
|
| 141 |
if (isConnectingToE2B) return { label: 'Connecting to E2B...', color: 'primary', icon: <CircularProgress size={16} thickness={5} sx={{ color: 'primary.main' }} /> };
|
|
|
|
| 148 |
// Extract model name from modelId (e.g., "Qwen/Qwen3-VL-8B-Instruct" -> "Qwen3-VL-8B-Instruct")
|
| 149 |
const modelName = trace?.modelId?.split('/').pop() || 'Unknown Model';
|
| 150 |
|
| 151 |
+
// Handler for emergency stop
|
| 152 |
+
const handleEmergencyStop = () => {
|
| 153 |
+
const stopTask = (window as Window & { __stopCurrentTask?: () => void }).__stopCurrentTask;
|
| 154 |
+
if (stopTask) {
|
| 155 |
+
stopTask();
|
| 156 |
+
}
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
return (
|
| 160 |
<AppBar
|
| 161 |
position="static"
|
|
|
|
| 203 |
</Typography>
|
| 204 |
</Box>
|
| 205 |
|
| 206 |
+
{/* Right side: Emergency Stop + Dark Mode */}
|
| 207 |
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
| 208 |
+
{/* Emergency Stop Button - Only show when agent is processing */}
|
| 209 |
+
{isAgentProcessing && (
|
| 210 |
+
<Button
|
| 211 |
+
onClick={handleEmergencyStop}
|
| 212 |
+
variant="outlined"
|
| 213 |
+
size="small"
|
| 214 |
+
startIcon={<StopCircleIcon />}
|
| 215 |
+
sx={{
|
| 216 |
+
color: 'error.main',
|
| 217 |
+
borderColor: 'error.main',
|
| 218 |
+
backgroundColor: 'transparent',
|
| 219 |
+
fontWeight: 600,
|
| 220 |
+
fontSize: '0.8rem',
|
| 221 |
+
px: 1.5,
|
| 222 |
+
py: 0.5,
|
| 223 |
+
textTransform: 'none',
|
| 224 |
+
'&:hover': {
|
| 225 |
+
backgroundColor: 'error.50',
|
| 226 |
+
borderColor: 'error.dark',
|
| 227 |
+
},
|
| 228 |
+
}}
|
| 229 |
+
>
|
| 230 |
+
Stop
|
| 231 |
+
</Button>
|
| 232 |
+
)}
|
| 233 |
+
|
| 234 |
<IconButton
|
| 235 |
onClick={toggleDarkMode}
|
| 236 |
size="small"
|
cua2-front/src/components/sandbox/SandboxViewer.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { selectError, selectFinalStep, selectSteps, selectTrace, useAgentStore }
|
|
| 4 |
import { AgentStep, AgentTraceMetadata } from '@/types/agent';
|
| 5 |
import ImageIcon from '@mui/icons-material/Image';
|
| 6 |
import MonitorIcon from '@mui/icons-material/Monitor';
|
| 7 |
-
import
|
| 8 |
import { Box, Button, CircularProgress, keyframes, Typography } from '@mui/material';
|
| 9 |
import React from 'react';
|
| 10 |
import { useNavigate } from 'react-router-dom';
|
|
@@ -58,6 +58,7 @@ export const SandboxViewer: React.FC<SandboxViewerProps> = ({
|
|
| 58 |
trace,
|
| 59 |
steps: steps || [],
|
| 60 |
metadata: finalStep?.metadata || metadata,
|
|
|
|
| 61 |
});
|
| 62 |
|
| 63 |
// Extract final_answer from the last step, or fallback to last thought
|
|
@@ -127,7 +128,7 @@ export const SandboxViewer: React.FC<SandboxViewerProps> = ({
|
|
| 127 |
position: 'relative',
|
| 128 |
border: '1px solid',
|
| 129 |
borderColor: showStatus
|
| 130 |
-
? (finalStep?.type === 'failure' ? 'error.main' : 'success.main')
|
| 131 |
: ((vncUrl || isAgentProcessing) && !selectedStep && !showStatus ? 'primary.main' : 'divider'),
|
| 132 |
borderRadius: '12px',
|
| 133 |
backgroundColor: 'background.paper',
|
|
@@ -191,7 +192,7 @@ export const SandboxViewer: React.FC<SandboxViewerProps> = ({
|
|
| 191 |
// Go Live Button when viewing a specific step
|
| 192 |
<Button
|
| 193 |
onClick={handleGoLive}
|
| 194 |
-
startIcon={<
|
| 195 |
sx={{
|
| 196 |
position: 'absolute',
|
| 197 |
top: 12,
|
|
@@ -307,6 +308,7 @@ export const SandboxViewer: React.FC<SandboxViewerProps> = ({
|
|
| 307 |
src={vncUrl}
|
| 308 |
style={{ width: '100%', height: '100%', border: 'none' }}
|
| 309 |
title="OS Stream"
|
|
|
|
| 310 |
/>
|
| 311 |
) : isAgentProcessing ? (
|
| 312 |
// Loading state
|
|
|
|
| 4 |
import { AgentStep, AgentTraceMetadata } from '@/types/agent';
|
| 5 |
import ImageIcon from '@mui/icons-material/Image';
|
| 6 |
import MonitorIcon from '@mui/icons-material/Monitor';
|
| 7 |
+
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
| 8 |
import { Box, Button, CircularProgress, keyframes, Typography } from '@mui/material';
|
| 9 |
import React from 'react';
|
| 10 |
import { useNavigate } from 'react-router-dom';
|
|
|
|
| 58 |
trace,
|
| 59 |
steps: steps || [],
|
| 60 |
metadata: finalStep?.metadata || metadata,
|
| 61 |
+
finalStep,
|
| 62 |
});
|
| 63 |
|
| 64 |
// Extract final_answer from the last step, or fallback to last thought
|
|
|
|
| 128 |
position: 'relative',
|
| 129 |
border: '1px solid',
|
| 130 |
borderColor: showStatus
|
| 131 |
+
? ((finalStep?.type === 'failure' || finalStep?.type === 'sandbox_timeout') ? 'error.main' : 'success.main')
|
| 132 |
: ((vncUrl || isAgentProcessing) && !selectedStep && !showStatus ? 'primary.main' : 'divider'),
|
| 133 |
borderRadius: '12px',
|
| 134 |
backgroundColor: 'background.paper',
|
|
|
|
| 192 |
// Go Live Button when viewing a specific step
|
| 193 |
<Button
|
| 194 |
onClick={handleGoLive}
|
| 195 |
+
startIcon={<PlayCircleIcon sx={{ fontSize: 20 }} />}
|
| 196 |
sx={{
|
| 197 |
position: 'absolute',
|
| 198 |
top: 12,
|
|
|
|
| 308 |
src={vncUrl}
|
| 309 |
style={{ width: '100%', height: '100%', border: 'none' }}
|
| 310 |
title="OS Stream"
|
| 311 |
+
lang="en"
|
| 312 |
/>
|
| 313 |
) : isAgentProcessing ? (
|
| 314 |
// Loading state
|
cua2-front/src/components/sandbox/completionview/CompletionView.tsx
CHANGED
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
| 2 |
import { Box, Typography, Button, Divider, Alert, Paper } from '@mui/material';
|
| 3 |
import CheckIcon from '@mui/icons-material/Check';
|
| 4 |
import CloseIcon from '@mui/icons-material/Close';
|
|
|
|
|
|
|
| 5 |
import AddIcon from '@mui/icons-material/Add';
|
| 6 |
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
| 7 |
import AssignmentIcon from '@mui/icons-material/Assignment';
|
|
@@ -40,8 +42,43 @@ export const CompletionView: React.FC<CompletionViewProps> = ({
|
|
| 40 |
onDownloadJson,
|
| 41 |
onBackToHome,
|
| 42 |
}) => {
|
| 43 |
-
const
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
// Format model name for display
|
| 47 |
const formatModelName = (modelId: string) => {
|
|
@@ -69,32 +106,32 @@ export const CompletionView: React.FC<CompletionViewProps> = ({
|
|
| 69 |
width: 40,
|
| 70 |
height: 40,
|
| 71 |
borderRadius: '50%',
|
| 72 |
-
backgroundColor:
|
| 73 |
display: 'flex',
|
| 74 |
alignItems: 'center',
|
| 75 |
justifyContent: 'center',
|
| 76 |
-
boxShadow: (theme) =>
|
| 77 |
-
|
| 78 |
-
?
|
| 79 |
-
:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
}}
|
| 81 |
>
|
| 82 |
-
{
|
| 83 |
-
<CheckIcon sx={{ fontSize: 24, color: 'white' }} />
|
| 84 |
-
) : (
|
| 85 |
-
<CloseIcon sx={{ fontSize: 24, color: 'white' }} />
|
| 86 |
-
)}
|
| 87 |
</Box>
|
| 88 |
<Typography
|
| 89 |
variant="h6"
|
| 90 |
sx={{
|
| 91 |
fontWeight: 700,
|
| 92 |
-
color:
|
| 93 |
fontSize: '1.1rem',
|
| 94 |
letterSpacing: '-0.5px',
|
| 95 |
}}
|
| 96 |
>
|
| 97 |
-
{
|
| 98 |
</Typography>
|
| 99 |
</Box>
|
| 100 |
</Box>
|
|
|
|
| 2 |
import { Box, Typography, Button, Divider, Alert, Paper } from '@mui/material';
|
| 3 |
import CheckIcon from '@mui/icons-material/Check';
|
| 4 |
import CloseIcon from '@mui/icons-material/Close';
|
| 5 |
+
import StopCircleIcon from '@mui/icons-material/StopCircle';
|
| 6 |
+
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
| 7 |
import AddIcon from '@mui/icons-material/Add';
|
| 8 |
import SmartToyIcon from '@mui/icons-material/SmartToy';
|
| 9 |
import AssignmentIcon from '@mui/icons-material/Assignment';
|
|
|
|
| 42 |
onDownloadJson,
|
| 43 |
onBackToHome,
|
| 44 |
}) => {
|
| 45 |
+
const getStatusConfig = () => {
|
| 46 |
+
switch (finalStep.type) {
|
| 47 |
+
case 'success':
|
| 48 |
+
return {
|
| 49 |
+
icon: <CheckIcon sx={{ fontSize: 28 }} />,
|
| 50 |
+
title: 'Task Completed Successfully!',
|
| 51 |
+
color: 'success.main',
|
| 52 |
+
};
|
| 53 |
+
case 'stopped':
|
| 54 |
+
return {
|
| 55 |
+
icon: <StopCircleIcon sx={{ fontSize: 28 }} />,
|
| 56 |
+
title: 'Task Stopped',
|
| 57 |
+
color: 'warning.main',
|
| 58 |
+
};
|
| 59 |
+
case 'max_steps_reached':
|
| 60 |
+
return {
|
| 61 |
+
icon: <HourglassEmptyIcon sx={{ fontSize: 28 }} />,
|
| 62 |
+
title: 'Maximum Steps Reached',
|
| 63 |
+
color: 'warning.main',
|
| 64 |
+
};
|
| 65 |
+
case 'sandbox_timeout':
|
| 66 |
+
return {
|
| 67 |
+
icon: <AccessTimeIcon sx={{ fontSize: 28 }} />,
|
| 68 |
+
title: 'Sandbox Timeout',
|
| 69 |
+
color: 'error.main',
|
| 70 |
+
};
|
| 71 |
+
case 'failure':
|
| 72 |
+
default:
|
| 73 |
+
return {
|
| 74 |
+
icon: <CloseIcon sx={{ fontSize: 28 }} />,
|
| 75 |
+
title: 'Task Failed',
|
| 76 |
+
color: 'error.main',
|
| 77 |
+
};
|
| 78 |
+
}
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const statusConfig = getStatusConfig();
|
| 82 |
|
| 83 |
// Format model name for display
|
| 84 |
const formatModelName = (modelId: string) => {
|
|
|
|
| 106 |
width: 40,
|
| 107 |
height: 40,
|
| 108 |
borderRadius: '50%',
|
| 109 |
+
backgroundColor: statusConfig.color,
|
| 110 |
display: 'flex',
|
| 111 |
alignItems: 'center',
|
| 112 |
justifyContent: 'center',
|
| 113 |
+
boxShadow: (theme) => {
|
| 114 |
+
const rgba = finalStep.type === 'success'
|
| 115 |
+
? '102, 187, 106'
|
| 116 |
+
: (finalStep.type === 'failure' || finalStep.type === 'sandbox_timeout')
|
| 117 |
+
? '244, 67, 54'
|
| 118 |
+
: '255, 152, 0';
|
| 119 |
+
return `0 2px 8px ${theme.palette.mode === 'dark' ? `rgba(${rgba}, 0.3)` : `rgba(${rgba}, 0.2)`}`;
|
| 120 |
+
},
|
| 121 |
}}
|
| 122 |
>
|
| 123 |
+
{React.cloneElement(statusConfig.icon, { sx: { fontSize: 24, color: 'white' } })}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
</Box>
|
| 125 |
<Typography
|
| 126 |
variant="h6"
|
| 127 |
sx={{
|
| 128 |
fontWeight: 700,
|
| 129 |
+
color: statusConfig.color,
|
| 130 |
fontSize: '1.1rem',
|
| 131 |
letterSpacing: '-0.5px',
|
| 132 |
}}
|
| 133 |
>
|
| 134 |
+
{statusConfig.title}
|
| 135 |
</Typography>
|
| 136 |
</Box>
|
| 137 |
</Box>
|
cua2-front/src/components/steps/FinalStepCard.tsx
CHANGED
|
@@ -3,6 +3,9 @@ import React from 'react';
|
|
| 3 |
import { Card, CardContent, Box, Typography } from '@mui/material';
|
| 4 |
import CheckIcon from '@mui/icons-material/Check';
|
| 5 |
import CloseIcon from '@mui/icons-material/Close';
|
|
|
|
|
|
|
|
|
|
| 6 |
import { useAgentStore } from '@/stores/agentStore';
|
| 7 |
|
| 8 |
interface FinalStepCardProps {
|
|
@@ -13,7 +16,43 @@ interface FinalStepCardProps {
|
|
| 13 |
export const FinalStepCard: React.FC<FinalStepCardProps> = ({ finalStep, isActive = false }) => {
|
| 14 |
const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
|
| 15 |
|
| 16 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
const handleClick = () => {
|
| 19 |
// Clicking on final step goes to live mode (null)
|
|
@@ -28,40 +67,36 @@ export const FinalStepCard: React.FC<FinalStepCardProps> = ({ finalStep, isActiv
|
|
| 28 |
backgroundColor: 'background.paper',
|
| 29 |
border: '1px solid',
|
| 30 |
borderColor: (theme) => `${isActive
|
| 31 |
-
?
|
| 32 |
: theme.palette.divider} !important`,
|
| 33 |
borderRadius: 1.5,
|
| 34 |
transition: 'all 0.2s ease',
|
| 35 |
cursor: 'pointer',
|
| 36 |
boxShadow: isActive
|
| 37 |
-
? (theme) =>
|
| 38 |
-
|
| 39 |
-
|
| 40 |
: 'none',
|
| 41 |
'&:hover': {
|
| 42 |
-
borderColor: (theme) => `${
|
| 43 |
-
boxShadow: (theme) =>
|
| 44 |
-
? `
|
| 45 |
-
: `
|
| 46 |
},
|
| 47 |
}}
|
| 48 |
>
|
| 49 |
<CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 } }}>
|
| 50 |
{/* Header with icon */}
|
| 51 |
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
| 52 |
-
{
|
| 53 |
-
<CheckIcon sx={{ fontSize: 20, color: 'success.main' }} />
|
| 54 |
-
) : (
|
| 55 |
-
<CloseIcon sx={{ fontSize: 20, color: 'error.main' }} />
|
| 56 |
-
)}
|
| 57 |
<Typography
|
| 58 |
sx={{
|
| 59 |
fontSize: '0.85rem',
|
| 60 |
fontWeight: 700,
|
| 61 |
-
color:
|
| 62 |
}}
|
| 63 |
>
|
| 64 |
-
{
|
| 65 |
</Typography>
|
| 66 |
</Box>
|
| 67 |
</CardContent>
|
|
|
|
| 3 |
import { Card, CardContent, Box, Typography } from '@mui/material';
|
| 4 |
import CheckIcon from '@mui/icons-material/Check';
|
| 5 |
import CloseIcon from '@mui/icons-material/Close';
|
| 6 |
+
import StopCircleIcon from '@mui/icons-material/StopCircle';
|
| 7 |
+
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
| 8 |
+
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
| 9 |
import { useAgentStore } from '@/stores/agentStore';
|
| 10 |
|
| 11 |
interface FinalStepCardProps {
|
|
|
|
| 16 |
export const FinalStepCard: React.FC<FinalStepCardProps> = ({ finalStep, isActive = false }) => {
|
| 17 |
const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex);
|
| 18 |
|
| 19 |
+
const getStatusConfig = () => {
|
| 20 |
+
switch (finalStep.type) {
|
| 21 |
+
case 'success':
|
| 22 |
+
return {
|
| 23 |
+
icon: <CheckIcon sx={{ fontSize: 20, color: 'success.main' }} />,
|
| 24 |
+
label: 'Task completed',
|
| 25 |
+
color: 'success',
|
| 26 |
+
};
|
| 27 |
+
case 'stopped':
|
| 28 |
+
return {
|
| 29 |
+
icon: <StopCircleIcon sx={{ fontSize: 20, color: 'warning.main' }} />,
|
| 30 |
+
label: 'Task stopped',
|
| 31 |
+
color: 'warning',
|
| 32 |
+
};
|
| 33 |
+
case 'max_steps_reached':
|
| 34 |
+
return {
|
| 35 |
+
icon: <HourglassEmptyIcon sx={{ fontSize: 20, color: 'warning.main' }} />,
|
| 36 |
+
label: 'Max steps reached',
|
| 37 |
+
color: 'warning',
|
| 38 |
+
};
|
| 39 |
+
case 'sandbox_timeout':
|
| 40 |
+
return {
|
| 41 |
+
icon: <AccessTimeIcon sx={{ fontSize: 20, color: 'error.main' }} />,
|
| 42 |
+
label: 'Sandbox timeout',
|
| 43 |
+
color: 'error',
|
| 44 |
+
};
|
| 45 |
+
case 'failure':
|
| 46 |
+
default:
|
| 47 |
+
return {
|
| 48 |
+
icon: <CloseIcon sx={{ fontSize: 20, color: 'error.main' }} />,
|
| 49 |
+
label: 'Task failed',
|
| 50 |
+
color: 'error',
|
| 51 |
+
};
|
| 52 |
+
}
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
const statusConfig = getStatusConfig();
|
| 56 |
|
| 57 |
const handleClick = () => {
|
| 58 |
// Clicking on final step goes to live mode (null)
|
|
|
|
| 67 |
backgroundColor: 'background.paper',
|
| 68 |
border: '1px solid',
|
| 69 |
borderColor: (theme) => `${isActive
|
| 70 |
+
? theme.palette[statusConfig.color].main
|
| 71 |
: theme.palette.divider} !important`,
|
| 72 |
borderRadius: 1.5,
|
| 73 |
transition: 'all 0.2s ease',
|
| 74 |
cursor: 'pointer',
|
| 75 |
boxShadow: isActive
|
| 76 |
+
? (theme) => `0 2px 8px ${theme.palette.mode === 'dark'
|
| 77 |
+
? `rgba(${statusConfig.color === 'success' ? '102, 187, 106' : statusConfig.color === 'error' ? '244, 67, 54' : '255, 152, 0'}, 0.3)`
|
| 78 |
+
: `rgba(${statusConfig.color === 'success' ? '102, 187, 106' : statusConfig.color === 'error' ? '244, 67, 54' : '255, 152, 0'}, 0.2)`}`
|
| 79 |
: 'none',
|
| 80 |
'&:hover': {
|
| 81 |
+
borderColor: (theme) => `${theme.palette[statusConfig.color].main} !important`,
|
| 82 |
+
boxShadow: (theme) => `0 2px 8px ${theme.palette.mode === 'dark'
|
| 83 |
+
? `rgba(${statusConfig.color === 'success' ? '102, 187, 106' : statusConfig.color === 'error' ? '244, 67, 54' : '255, 152, 0'}, 0.2)`
|
| 84 |
+
: `rgba(${statusConfig.color === 'success' ? '102, 187, 106' : statusConfig.color === 'error' ? '244, 67, 54' : '255, 152, 0'}, 0.1)`}`,
|
| 85 |
},
|
| 86 |
}}
|
| 87 |
>
|
| 88 |
<CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 } }}>
|
| 89 |
{/* Header with icon */}
|
| 90 |
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>
|
| 91 |
+
{statusConfig.icon}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
<Typography
|
| 93 |
sx={{
|
| 94 |
fontSize: '0.85rem',
|
| 95 |
fontWeight: 700,
|
| 96 |
+
color: `${statusConfig.color}.main`,
|
| 97 |
}}
|
| 98 |
>
|
| 99 |
+
{statusConfig.label}
|
| 100 |
</Typography>
|
| 101 |
</Box>
|
| 102 |
</CardContent>
|
cua2-front/src/components/steps/StepCard.tsx
CHANGED
|
@@ -339,7 +339,14 @@ export const StepCard: React.FC<StepCardProps> = ({ step, index, isLatest = fals
|
|
| 339 |
|
| 340 |
{/* Error */}
|
| 341 |
{step.error && (
|
| 342 |
-
<Box sx={{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
<Typography
|
| 344 |
variant="caption"
|
| 345 |
sx={{
|
|
|
|
| 339 |
|
| 340 |
{/* Error */}
|
| 341 |
{step.error && (
|
| 342 |
+
<Box sx={{
|
| 343 |
+
mt: 1.5,
|
| 344 |
+
p: 1,
|
| 345 |
+
borderRadius: 1,
|
| 346 |
+
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.08)',
|
| 347 |
+
border: '1px solid',
|
| 348 |
+
borderColor: 'error.main'
|
| 349 |
+
}}>
|
| 350 |
<Typography
|
| 351 |
variant="caption"
|
| 352 |
sx={{
|
cua2-front/src/components/steps/StepsList.tsx
CHANGED
|
@@ -30,19 +30,25 @@ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
|
|
| 30 |
// Check if final step is active (when selectedStepIndex is null and finalStep exists and trace is not running)
|
| 31 |
const isFinalStepActive = selectedStepIndex === null && finalStep && !trace?.isRunning;
|
| 32 |
|
|
|
|
|
|
|
|
|
|
| 33 |
// Determine the active step index
|
| 34 |
// If a specific step is selected, use that
|
| 35 |
// If the final step is active, no normal step should be active
|
|
|
|
| 36 |
// Otherwise, show the last step as active
|
| 37 |
const activeStepIndex = selectedStepIndex !== null
|
| 38 |
? selectedStepIndex
|
| 39 |
: isFinalStepActive
|
| 40 |
? null // When final step is active, no normal step is active
|
| 41 |
-
:
|
| 42 |
-
?
|
| 43 |
-
: (trace?.steps && trace.steps.length > 0)
|
| 44 |
? trace.steps.length - 1
|
| 45 |
-
:
|
|
|
|
|
|
|
| 46 |
|
| 47 |
// Manage ConnectionStepCard display:
|
| 48 |
// - Shows when isConnectingToE2B = true OR when we had a connection
|
|
@@ -101,44 +107,42 @@ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
|
|
| 101 |
};
|
| 102 |
}, [isAgentProcessing, isConnectingToE2B, finalStep]);
|
| 103 |
|
| 104 |
-
// Auto-scroll
|
| 105 |
useEffect(() => {
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
});
|
| 132 |
-
// Reset flag after scroll animation
|
| 133 |
-
setTimeout(() => {
|
| 134 |
-
isScrollingProgrammatically.current = false;
|
| 135 |
-
}, 500);
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
}
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
// Detect which step is visible when scrolling (steps → timeline)
|
| 144 |
useEffect(() => {
|
|
@@ -149,6 +153,9 @@ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
|
|
| 149 |
// Don't update if we're scrolling programmatically
|
| 150 |
if (isScrollingProgrammatically.current) return;
|
| 151 |
|
|
|
|
|
|
|
|
|
|
| 152 |
const containerRect = container.getBoundingClientRect();
|
| 153 |
const containerTop = containerRect.top;
|
| 154 |
const containerBottom = containerRect.bottom;
|
|
@@ -346,7 +353,7 @@ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
|
|
| 346 |
{/* Show thinking indicator after steps (appears 5 seconds after stream start) */}
|
| 347 |
{showThinkingCard && (
|
| 348 |
<Box data-step-index="thinking">
|
| 349 |
-
<ThinkingStepCard />
|
| 350 |
</Box>
|
| 351 |
)}
|
| 352 |
|
|
|
|
| 30 |
// Check if final step is active (when selectedStepIndex is null and finalStep exists and trace is not running)
|
| 31 |
const isFinalStepActive = selectedStepIndex === null && finalStep && !trace?.isRunning;
|
| 32 |
|
| 33 |
+
// Check if thinking card is active (when in live mode and thinking card is shown)
|
| 34 |
+
const isThinkingCardActive = selectedStepIndex === null && showThinkingCard;
|
| 35 |
+
|
| 36 |
// Determine the active step index
|
| 37 |
// If a specific step is selected, use that
|
| 38 |
// If the final step is active, no normal step should be active
|
| 39 |
+
// If the thinking card is active, no normal step should be active
|
| 40 |
// Otherwise, show the last step as active
|
| 41 |
const activeStepIndex = selectedStepIndex !== null
|
| 42 |
? selectedStepIndex
|
| 43 |
: isFinalStepActive
|
| 44 |
? null // When final step is active, no normal step is active
|
| 45 |
+
: isThinkingCardActive
|
| 46 |
+
? null // When thinking card is active, no normal step is active
|
| 47 |
+
: (trace?.steps && trace.steps.length > 0 && trace?.isRunning)
|
| 48 |
? trace.steps.length - 1
|
| 49 |
+
: (trace?.steps && trace.steps.length > 0)
|
| 50 |
+
? trace.steps.length - 1
|
| 51 |
+
: null;
|
| 52 |
|
| 53 |
// Manage ConnectionStepCard display:
|
| 54 |
// - Shows when isConnectingToE2B = true OR when we had a connection
|
|
|
|
| 107 |
};
|
| 108 |
}, [isAgentProcessing, isConnectingToE2B, finalStep]);
|
| 109 |
|
| 110 |
+
// Auto-scroll logic
|
| 111 |
useEffect(() => {
|
| 112 |
+
const container = containerRef.current;
|
| 113 |
+
if (!container) return;
|
| 114 |
+
|
| 115 |
+
isScrollingProgrammatically.current = true;
|
| 116 |
+
|
| 117 |
+
// Use setTimeout to ensure DOM has updated
|
| 118 |
+
setTimeout(() => {
|
| 119 |
+
if (!container) return;
|
| 120 |
+
|
| 121 |
+
// LIVE MODE: Always scroll to the bottom (last visible element)
|
| 122 |
+
if (selectedStepIndex === null) {
|
| 123 |
+
// Scroll to bottom
|
| 124 |
+
container.scrollTo({
|
| 125 |
+
top: container.scrollHeight,
|
| 126 |
+
behavior: 'smooth',
|
| 127 |
+
});
|
| 128 |
+
}
|
| 129 |
+
// NON-LIVE MODE: Scroll to selected step
|
| 130 |
+
else {
|
| 131 |
+
const selectedElement = container.querySelector(`[data-step-index="${selectedStepIndex}"]`);
|
| 132 |
+
if (selectedElement) {
|
| 133 |
+
selectedElement.scrollIntoView({
|
| 134 |
+
behavior: 'smooth',
|
| 135 |
+
block: 'center',
|
| 136 |
+
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// Reset flag after scroll animation
|
| 141 |
+
setTimeout(() => {
|
| 142 |
+
isScrollingProgrammatically.current = false;
|
| 143 |
+
}, 500);
|
| 144 |
+
}, 100);
|
| 145 |
+
}, [selectedStepIndex, trace?.steps?.length, showThinkingCard, finalStep]);
|
| 146 |
|
| 147 |
// Detect which step is visible when scrolling (steps → timeline)
|
| 148 |
useEffect(() => {
|
|
|
|
| 153 |
// Don't update if we're scrolling programmatically
|
| 154 |
if (isScrollingProgrammatically.current) return;
|
| 155 |
|
| 156 |
+
// Don't update if agent is running (stay in live mode)
|
| 157 |
+
if (trace?.isRunning) return;
|
| 158 |
+
|
| 159 |
const containerRect = container.getBoundingClientRect();
|
| 160 |
const containerTop = containerRect.top;
|
| 161 |
const containerBottom = containerRect.bottom;
|
|
|
|
| 353 |
{/* Show thinking indicator after steps (appears 5 seconds after stream start) */}
|
| 354 |
{showThinkingCard && (
|
| 355 |
<Box data-step-index="thinking">
|
| 356 |
+
<ThinkingStepCard isActive={isThinkingCardActive} />
|
| 357 |
</Box>
|
| 358 |
)}
|
| 359 |
|
cua2-front/src/components/steps/ThinkingStepCard.tsx
CHANGED
|
@@ -24,19 +24,25 @@ const backgroundPulse = keyframes`
|
|
| 24 |
}
|
| 25 |
`;
|
| 26 |
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
return (
|
| 30 |
<Card
|
| 31 |
elevation={0}
|
| 32 |
sx={{
|
| 33 |
backgroundColor: 'background.paper',
|
| 34 |
-
border: '
|
| 35 |
-
borderColor:
|
| 36 |
borderRadius: 1.5,
|
| 37 |
-
animation: `${borderPulse} 2s ease-in-out infinite
|
| 38 |
position: 'relative',
|
| 39 |
overflow: 'hidden',
|
|
|
|
|
|
|
| 40 |
'&::before': {
|
| 41 |
content: '""',
|
| 42 |
position: 'absolute',
|
|
@@ -44,7 +50,7 @@ export const ThinkingStepCard: React.FC = () => {
|
|
| 44 |
left: 0,
|
| 45 |
right: 0,
|
| 46 |
bottom: 0,
|
| 47 |
-
animation: `${backgroundPulse} 2s ease-in-out infinite
|
| 48 |
zIndex: 0,
|
| 49 |
},
|
| 50 |
}}
|
|
|
|
| 24 |
}
|
| 25 |
`;
|
| 26 |
|
| 27 |
+
interface ThinkingStepCardProps {
|
| 28 |
+
isActive?: boolean;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export const ThinkingStepCard: React.FC<ThinkingStepCardProps> = ({ isActive = false }) => {
|
| 32 |
|
| 33 |
return (
|
| 34 |
<Card
|
| 35 |
elevation={0}
|
| 36 |
sx={{
|
| 37 |
backgroundColor: 'background.paper',
|
| 38 |
+
border: '1px solid',
|
| 39 |
+
borderColor: (theme) => `${isActive ? theme.palette.primary.main : theme.palette.divider} !important`,
|
| 40 |
borderRadius: 1.5,
|
| 41 |
+
animation: isActive ? `${borderPulse} 2s ease-in-out infinite` : 'none',
|
| 42 |
position: 'relative',
|
| 43 |
overflow: 'hidden',
|
| 44 |
+
boxShadow: isActive ? (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.2)'}` : 'none',
|
| 45 |
+
transition: 'all 0.2s ease',
|
| 46 |
'&::before': {
|
| 47 |
content: '""',
|
| 48 |
position: 'absolute',
|
|
|
|
| 50 |
left: 0,
|
| 51 |
right: 0,
|
| 52 |
bottom: 0,
|
| 53 |
+
animation: isActive ? `${backgroundPulse} 2s ease-in-out infinite` : 'none',
|
| 54 |
zIndex: 0,
|
| 55 |
},
|
| 56 |
}}
|
cua2-front/src/components/timeline/Timeline.tsx
CHANGED
|
@@ -2,6 +2,9 @@ import React, { useRef, useEffect } from 'react';
|
|
| 2 |
import { Box, Typography, CircularProgress, Button } from '@mui/material';
|
| 3 |
import CheckIcon from '@mui/icons-material/Check';
|
| 4 |
import CloseIcon from '@mui/icons-material/Close';
|
|
|
|
|
|
|
|
|
|
| 5 |
import CableIcon from '@mui/icons-material/Cable';
|
| 6 |
import { AgentTraceMetadata } from '@/types/agent';
|
| 7 |
import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore';
|
|
@@ -23,8 +26,15 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 23 |
const showConnectionIndicator = isConnectingToE2B || isAgentProcessing || (metadata.numberOfSteps > 0) || finalStep;
|
| 24 |
|
| 25 |
// Generate array of steps with their status
|
| 26 |
-
//
|
| 27 |
-
const totalStepsToShow =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
const steps = Array.from({ length: totalStepsToShow }, (_, index) => ({
|
| 30 |
stepNumber: index + 1,
|
|
@@ -130,13 +140,14 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 130 |
'&::before': {
|
| 131 |
content: '""',
|
| 132 |
position: 'absolute',
|
| 133 |
-
left: "
|
| 134 |
-
// Calculate width to cover
|
| 135 |
-
width:
|
| 136 |
-
top: '
|
| 137 |
transform: 'translateY(-50%)',
|
|
|
|
| 138 |
height: '2px',
|
| 139 |
-
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.
|
| 140 |
zIndex: 0,
|
| 141 |
pointerEvents: 'none',
|
| 142 |
},
|
|
@@ -151,7 +162,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 151 |
flexDirection: 'column',
|
| 152 |
alignItems: 'center',
|
| 153 |
gap: 0.75,
|
| 154 |
-
minWidth:
|
| 155 |
flexShrink: 0,
|
| 156 |
position: 'relative',
|
| 157 |
zIndex: 1,
|
|
@@ -164,14 +175,16 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 164 |
display: 'flex',
|
| 165 |
alignItems: 'center',
|
| 166 |
justifyContent: 'center',
|
|
|
|
|
|
|
| 167 |
}}
|
| 168 |
>
|
| 169 |
{/* White background to hide the line */}
|
| 170 |
<Box
|
| 171 |
sx={{
|
| 172 |
position: 'absolute',
|
| 173 |
-
width:
|
| 174 |
-
height:
|
| 175 |
borderRadius: '50%',
|
| 176 |
backgroundColor: 'background.paper',
|
| 177 |
zIndex: 0,
|
|
@@ -246,14 +259,16 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 246 |
display: 'flex',
|
| 247 |
alignItems: 'center',
|
| 248 |
justifyContent: 'center',
|
|
|
|
|
|
|
| 249 |
}}
|
| 250 |
>
|
| 251 |
{/* White background to hide the line */}
|
| 252 |
<Box
|
| 253 |
sx={{
|
| 254 |
position: 'absolute',
|
| 255 |
-
width:
|
| 256 |
-
height:
|
| 257 |
borderRadius: '50%',
|
| 258 |
backgroundColor: 'background.paper',
|
| 259 |
zIndex: 0,
|
|
@@ -262,39 +277,76 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 262 |
|
| 263 |
{/* Step dot */}
|
| 264 |
{step.isCurrent ? (
|
| 265 |
-
<
|
| 266 |
-
size={20}
|
| 267 |
-
thickness={5}
|
| 268 |
sx={{
|
| 269 |
-
color: 'primary.main',
|
| 270 |
position: 'relative',
|
|
|
|
|
|
|
|
|
|
| 271 |
zIndex: 1,
|
| 272 |
}}
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
) : (
|
| 275 |
<Box
|
| 276 |
-
className="step-dot"
|
| 277 |
sx={{
|
| 278 |
-
|
| 279 |
-
height: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
|
| 280 |
-
borderRadius: '50%',
|
| 281 |
-
// Always keep steps in primary color (blue)
|
| 282 |
-
backgroundColor: step.isCompleted
|
| 283 |
-
? 'primary.main' // Blue for completed steps
|
| 284 |
-
: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.300', // Light grey for future steps
|
| 285 |
display: 'flex',
|
| 286 |
alignItems: 'center',
|
| 287 |
justifyContent: 'center',
|
| 288 |
-
transition: 'all 0.2s ease',
|
| 289 |
-
boxShadow: step.isCompleted || step.isSelected
|
| 290 |
-
? step.isSelected
|
| 291 |
-
? '0 0 8px rgba(255, 167, 38, 0.5)'
|
| 292 |
-
: '0 2px 4px rgba(0,0,0,0.1)'
|
| 293 |
-
: 'none',
|
| 294 |
-
position: 'relative',
|
| 295 |
zIndex: 1,
|
| 296 |
}}
|
| 297 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
)}
|
| 299 |
</Box>
|
| 300 |
|
|
@@ -303,13 +355,14 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 303 |
variant="caption"
|
| 304 |
sx={{
|
| 305 |
fontSize: '0.7rem',
|
| 306 |
-
fontWeight: step.
|
| 307 |
color: step.isCurrent
|
| 308 |
? 'primary.main'
|
| 309 |
: (step.isCompleted || step.isSelected
|
| 310 |
? 'text.primary'
|
| 311 |
: (theme) => theme.palette.mode === 'dark' ? 'grey.700' : 'grey.400'),
|
| 312 |
whiteSpace: 'nowrap',
|
|
|
|
| 313 |
}}
|
| 314 |
>
|
| 315 |
{step.stepNumber}
|
|
@@ -326,7 +379,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 326 |
flexDirection: 'column',
|
| 327 |
alignItems: 'center',
|
| 328 |
gap: 0.75,
|
| 329 |
-
minWidth:
|
| 330 |
flexShrink: 0,
|
| 331 |
position: 'relative',
|
| 332 |
zIndex: 1,
|
|
@@ -345,14 +398,16 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 345 |
display: 'flex',
|
| 346 |
alignItems: 'center',
|
| 347 |
justifyContent: 'center',
|
|
|
|
|
|
|
| 348 |
}}
|
| 349 |
>
|
| 350 |
{/* White background to hide the line */}
|
| 351 |
<Box
|
| 352 |
sx={{
|
| 353 |
position: 'absolute',
|
| 354 |
-
width:
|
| 355 |
-
height:
|
| 356 |
borderRadius: '50%',
|
| 357 |
backgroundColor: 'background.paper',
|
| 358 |
zIndex: 0,
|
|
@@ -363,10 +418,13 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 363 |
<Box
|
| 364 |
className="final-step-icon"
|
| 365 |
sx={{
|
| 366 |
-
width: selectedStepIndex === null ?
|
| 367 |
-
height: selectedStepIndex === null ?
|
| 368 |
borderRadius: '50%',
|
| 369 |
-
backgroundColor:
|
|
|
|
|
|
|
|
|
|
| 370 |
display: 'flex',
|
| 371 |
alignItems: 'center',
|
| 372 |
justifyContent: 'center',
|
|
@@ -374,7 +432,9 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 374 |
boxShadow: selectedStepIndex === null
|
| 375 |
? finalStep.type === 'success'
|
| 376 |
? '0 2px 8px rgba(102, 187, 106, 0.4)'
|
| 377 |
-
:
|
|
|
|
|
|
|
| 378 |
: '0 2px 4px rgba(0,0,0,0.1)',
|
| 379 |
position: 'relative',
|
| 380 |
zIndex: 1,
|
|
@@ -382,6 +442,12 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 382 |
>
|
| 383 |
{finalStep.type === 'success' ? (
|
| 384 |
<CheckIcon sx={{ fontSize: 14, color: 'white' }} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
) : (
|
| 386 |
<CloseIcon sx={{ fontSize: 14, color: 'white' }} />
|
| 387 |
)}
|
|
@@ -394,13 +460,20 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
|
|
| 394 |
sx={{
|
| 395 |
fontSize: '0.7rem',
|
| 396 |
fontWeight: selectedStepIndex === null ? 700 : 500,
|
| 397 |
-
color:
|
| 398 |
-
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
| 400 |
whiteSpace: 'nowrap',
|
| 401 |
}}
|
| 402 |
>
|
| 403 |
-
{finalStep.type === 'success' ? 'End' :
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
</Typography>
|
| 405 |
</Box>
|
| 406 |
)}
|
|
|
|
| 2 |
import { Box, Typography, CircularProgress, Button } from '@mui/material';
|
| 3 |
import CheckIcon from '@mui/icons-material/Check';
|
| 4 |
import CloseIcon from '@mui/icons-material/Close';
|
| 5 |
+
import StopCircleIcon from '@mui/icons-material/StopCircle';
|
| 6 |
+
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
| 7 |
+
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
| 8 |
import CableIcon from '@mui/icons-material/Cable';
|
| 9 |
import { AgentTraceMetadata } from '@/types/agent';
|
| 10 |
import { useAgentStore, selectSelectedStepIndex, selectFinalStep, selectIsConnectingToE2B, selectIsAgentProcessing } from '@/stores/agentStore';
|
|
|
|
| 26 |
const showConnectionIndicator = isConnectingToE2B || isAgentProcessing || (metadata.numberOfSteps > 0) || finalStep;
|
| 27 |
|
| 28 |
// Generate array of steps with their status
|
| 29 |
+
// Only show completed steps + current step if running
|
| 30 |
+
const totalStepsToShow = isRunning && !isConnectingToE2B
|
| 31 |
+
? metadata.numberOfSteps + 1 // Show completed steps + current step
|
| 32 |
+
: metadata.numberOfSteps; // Show only completed steps when not running
|
| 33 |
+
|
| 34 |
+
// Calculate total width for the line (including finalStep if present)
|
| 35 |
+
const lineWidth = finalStep
|
| 36 |
+
? `calc(${totalStepsToShow} * (40px + 12px) + 52px)` // Add space for finalStep (40px + 12px gap)
|
| 37 |
+
: `calc(${totalStepsToShow} * (40px + 12px))`;
|
| 38 |
|
| 39 |
const steps = Array.from({ length: totalStepsToShow }, (_, index) => ({
|
| 40 |
stepNumber: index + 1,
|
|
|
|
| 140 |
'&::before': {
|
| 141 |
content: '""',
|
| 142 |
position: 'absolute',
|
| 143 |
+
left: "25px",
|
| 144 |
+
// Calculate width to cover visible steps + finalStep if present
|
| 145 |
+
width: lineWidth,
|
| 146 |
+
top: '19.5px',
|
| 147 |
transform: 'translateY(-50%)',
|
| 148 |
+
transition: 'width 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
|
| 149 |
height: '2px',
|
| 150 |
+
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.3)',
|
| 151 |
zIndex: 0,
|
| 152 |
pointerEvents: 'none',
|
| 153 |
},
|
|
|
|
| 162 |
flexDirection: 'column',
|
| 163 |
alignItems: 'center',
|
| 164 |
gap: 0.75,
|
| 165 |
+
minWidth: 40,
|
| 166 |
flexShrink: 0,
|
| 167 |
position: 'relative',
|
| 168 |
zIndex: 1,
|
|
|
|
| 175 |
display: 'flex',
|
| 176 |
alignItems: 'center',
|
| 177 |
justifyContent: 'center',
|
| 178 |
+
height: 28,
|
| 179 |
+
width: 28,
|
| 180 |
}}
|
| 181 |
>
|
| 182 |
{/* White background to hide the line */}
|
| 183 |
<Box
|
| 184 |
sx={{
|
| 185 |
position: 'absolute',
|
| 186 |
+
width: 28,
|
| 187 |
+
height: 28,
|
| 188 |
borderRadius: '50%',
|
| 189 |
backgroundColor: 'background.paper',
|
| 190 |
zIndex: 0,
|
|
|
|
| 259 |
display: 'flex',
|
| 260 |
alignItems: 'center',
|
| 261 |
justifyContent: 'center',
|
| 262 |
+
height: 28,
|
| 263 |
+
width: 28,
|
| 264 |
}}
|
| 265 |
>
|
| 266 |
{/* White background to hide the line */}
|
| 267 |
<Box
|
| 268 |
sx={{
|
| 269 |
position: 'absolute',
|
| 270 |
+
width: 28,
|
| 271 |
+
height: 28,
|
| 272 |
borderRadius: '50%',
|
| 273 |
backgroundColor: 'background.paper',
|
| 274 |
zIndex: 0,
|
|
|
|
| 277 |
|
| 278 |
{/* Step dot */}
|
| 279 |
{step.isCurrent ? (
|
| 280 |
+
<Box
|
|
|
|
|
|
|
| 281 |
sx={{
|
|
|
|
| 282 |
position: 'relative',
|
| 283 |
+
display: 'flex',
|
| 284 |
+
alignItems: 'center',
|
| 285 |
+
justifyContent: 'center',
|
| 286 |
zIndex: 1,
|
| 287 |
}}
|
| 288 |
+
>
|
| 289 |
+
<CircularProgress
|
| 290 |
+
size={20}
|
| 291 |
+
thickness={5}
|
| 292 |
+
sx={{
|
| 293 |
+
color: 'primary.main',
|
| 294 |
+
position: 'absolute',
|
| 295 |
+
}}
|
| 296 |
+
/>
|
| 297 |
+
<Box
|
| 298 |
+
sx={{
|
| 299 |
+
width: 8,
|
| 300 |
+
height: 8,
|
| 301 |
+
borderRadius: '50%',
|
| 302 |
+
backgroundColor: 'white',
|
| 303 |
+
position: 'absolute',
|
| 304 |
+
pointerEvents: 'none',
|
| 305 |
+
boxShadow: '0 0 4px rgba(0,0,0,0.2)',
|
| 306 |
+
}}
|
| 307 |
+
/>
|
| 308 |
+
</Box>
|
| 309 |
) : (
|
| 310 |
<Box
|
|
|
|
| 311 |
sx={{
|
| 312 |
+
position: 'relative',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
display: 'flex',
|
| 314 |
alignItems: 'center',
|
| 315 |
justifyContent: 'center',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
zIndex: 1,
|
| 317 |
}}
|
| 318 |
+
>
|
| 319 |
+
<Box
|
| 320 |
+
className="step-dot"
|
| 321 |
+
sx={{
|
| 322 |
+
width: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
|
| 323 |
+
height: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
|
| 324 |
+
borderRadius: '50%',
|
| 325 |
+
// Always keep steps in primary color (blue)
|
| 326 |
+
backgroundColor: step.isCompleted
|
| 327 |
+
? 'primary.main' // Blue for completed steps
|
| 328 |
+
: (theme) => theme.palette.mode === 'dark' ? 'grey.800' : 'grey.300', // Light grey for future steps
|
| 329 |
+
transition: 'all 0.2s ease',
|
| 330 |
+
boxShadow: step.isCompleted || step.isSelected
|
| 331 |
+
? step.isSelected
|
| 332 |
+
? '0 0 8px rgba(255, 167, 38, 0.5)'
|
| 333 |
+
: '0 2px 4px rgba(0,0,0,0.1)'
|
| 334 |
+
: 'none',
|
| 335 |
+
}}
|
| 336 |
+
/>
|
| 337 |
+
{/* White dot for selected step */}
|
| 338 |
+
{step.isSelected && (
|
| 339 |
+
<Box
|
| 340 |
+
sx={{
|
| 341 |
+
width: 8,
|
| 342 |
+
height: 8,
|
| 343 |
+
borderRadius: '50%',
|
| 344 |
+
backgroundColor: 'white',
|
| 345 |
+
position: 'absolute',
|
| 346 |
+
}}
|
| 347 |
+
/>
|
| 348 |
+
)}
|
| 349 |
+
</Box>
|
| 350 |
)}
|
| 351 |
</Box>
|
| 352 |
|
|
|
|
| 355 |
variant="caption"
|
| 356 |
sx={{
|
| 357 |
fontSize: '0.7rem',
|
| 358 |
+
fontWeight: step.isSelected || step.isCurrent ? 900 : 400,
|
| 359 |
color: step.isCurrent
|
| 360 |
? 'primary.main'
|
| 361 |
: (step.isCompleted || step.isSelected
|
| 362 |
? 'text.primary'
|
| 363 |
: (theme) => theme.palette.mode === 'dark' ? 'grey.700' : 'grey.400'),
|
| 364 |
whiteSpace: 'nowrap',
|
| 365 |
+
lineHeight: 1,
|
| 366 |
}}
|
| 367 |
>
|
| 368 |
{step.stepNumber}
|
|
|
|
| 379 |
flexDirection: 'column',
|
| 380 |
alignItems: 'center',
|
| 381 |
gap: 0.75,
|
| 382 |
+
minWidth: 40,
|
| 383 |
flexShrink: 0,
|
| 384 |
position: 'relative',
|
| 385 |
zIndex: 1,
|
|
|
|
| 398 |
display: 'flex',
|
| 399 |
alignItems: 'center',
|
| 400 |
justifyContent: 'center',
|
| 401 |
+
height: 28,
|
| 402 |
+
width: 28,
|
| 403 |
}}
|
| 404 |
>
|
| 405 |
{/* White background to hide the line */}
|
| 406 |
<Box
|
| 407 |
sx={{
|
| 408 |
position: 'absolute',
|
| 409 |
+
width: 28,
|
| 410 |
+
height: 28,
|
| 411 |
borderRadius: '50%',
|
| 412 |
backgroundColor: 'background.paper',
|
| 413 |
zIndex: 0,
|
|
|
|
| 418 |
<Box
|
| 419 |
className="final-step-icon"
|
| 420 |
sx={{
|
| 421 |
+
width: selectedStepIndex === null ? 20 : 18,
|
| 422 |
+
height: selectedStepIndex === null ? 20 : 18,
|
| 423 |
borderRadius: '50%',
|
| 424 |
+
backgroundColor:
|
| 425 |
+
finalStep.type === 'success' ? 'success.main' :
|
| 426 |
+
finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached' ? 'warning.main' :
|
| 427 |
+
'error.main',
|
| 428 |
display: 'flex',
|
| 429 |
alignItems: 'center',
|
| 430 |
justifyContent: 'center',
|
|
|
|
| 432 |
boxShadow: selectedStepIndex === null
|
| 433 |
? finalStep.type === 'success'
|
| 434 |
? '0 2px 8px rgba(102, 187, 106, 0.4)'
|
| 435 |
+
: finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached'
|
| 436 |
+
? '0 2px 8px rgba(255, 152, 0, 0.4)'
|
| 437 |
+
: '0 2px 8px rgba(244, 67, 54, 0.4)'
|
| 438 |
: '0 2px 4px rgba(0,0,0,0.1)',
|
| 439 |
position: 'relative',
|
| 440 |
zIndex: 1,
|
|
|
|
| 442 |
>
|
| 443 |
{finalStep.type === 'success' ? (
|
| 444 |
<CheckIcon sx={{ fontSize: 14, color: 'white' }} />
|
| 445 |
+
) : finalStep.type === 'stopped' ? (
|
| 446 |
+
<StopCircleIcon sx={{ fontSize: 14, color: 'white' }} />
|
| 447 |
+
) : finalStep.type === 'max_steps_reached' ? (
|
| 448 |
+
<HourglassEmptyIcon sx={{ fontSize: 14, color: 'white' }} />
|
| 449 |
+
) : finalStep.type === 'sandbox_timeout' ? (
|
| 450 |
+
<AccessTimeIcon sx={{ fontSize: 14, color: 'white' }} />
|
| 451 |
) : (
|
| 452 |
<CloseIcon sx={{ fontSize: 14, color: 'white' }} />
|
| 453 |
)}
|
|
|
|
| 460 |
sx={{
|
| 461 |
fontSize: '0.7rem',
|
| 462 |
fontWeight: selectedStepIndex === null ? 700 : 500,
|
| 463 |
+
color:
|
| 464 |
+
finalStep.type === 'success'
|
| 465 |
+
? (selectedStepIndex === null ? 'text.primary' : 'text.secondary')
|
| 466 |
+
: finalStep.type === 'stopped' || finalStep.type === 'max_steps_reached'
|
| 467 |
+
? 'warning.main'
|
| 468 |
+
: 'error.main',
|
| 469 |
whiteSpace: 'nowrap',
|
| 470 |
}}
|
| 471 |
>
|
| 472 |
+
{finalStep.type === 'success' ? 'End' :
|
| 473 |
+
finalStep.type === 'stopped' ? 'Stopped' :
|
| 474 |
+
finalStep.type === 'max_steps_reached' ? 'Max Steps' :
|
| 475 |
+
finalStep.type === 'sandbox_timeout' ? 'Timeout' :
|
| 476 |
+
'Failed'}
|
| 477 |
</Typography>
|
| 478 |
</Box>
|
| 479 |
)}
|
cua2-front/src/hooks/useAgentWebSocket.ts
CHANGED
|
@@ -70,8 +70,8 @@ export const useAgentWebSocket = ({ url }: UseAgentWebSocketOptions) => {
|
|
| 70 |
case 'agent_complete':
|
| 71 |
setIsAgentProcessing(false);
|
| 72 |
setIsConnectingToE2B(false);
|
| 73 |
-
completeTrace(event.traceMetadata);
|
| 74 |
-
console.log('Agent complete received:', event.traceMetadata);
|
| 75 |
break;
|
| 76 |
|
| 77 |
case 'agent_error':
|
|
@@ -157,9 +157,24 @@ export const useAgentWebSocket = ({ url }: UseAgentWebSocketOptions) => {
|
|
| 157 |
};
|
| 158 |
}, [setTrace, setIsAgentProcessing, setIsConnectingToE2B, sendMessage, resetAgent]);
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
return {
|
| 161 |
isConnected,
|
| 162 |
connectionState,
|
| 163 |
manualReconnect,
|
|
|
|
| 164 |
};
|
| 165 |
};
|
|
|
|
| 70 |
case 'agent_complete':
|
| 71 |
setIsAgentProcessing(false);
|
| 72 |
setIsConnectingToE2B(false);
|
| 73 |
+
completeTrace(event.traceMetadata, event.final_state);
|
| 74 |
+
console.log('Agent complete received:', event.traceMetadata, 'Final state:', event.final_state);
|
| 75 |
break;
|
| 76 |
|
| 77 |
case 'agent_error':
|
|
|
|
| 157 |
};
|
| 158 |
}, [setTrace, setIsAgentProcessing, setIsConnectingToE2B, sendMessage, resetAgent]);
|
| 159 |
|
| 160 |
+
// Function to stop the current task
|
| 161 |
+
const stopCurrentTask = useCallback(() => {
|
| 162 |
+
const trace = useAgentStore.getState().trace;
|
| 163 |
+
if (trace?.id && trace.isRunning) {
|
| 164 |
+
sendMessage({
|
| 165 |
+
type: 'stop_task',
|
| 166 |
+
trace_id: trace.id,
|
| 167 |
+
});
|
| 168 |
+
console.log('Stop task sent for trace:', trace.id);
|
| 169 |
+
|
| 170 |
+
// Don't update UI state here - wait for backend to send agent_complete with final_state='stopped'
|
| 171 |
+
}
|
| 172 |
+
}, [sendMessage]);
|
| 173 |
+
|
| 174 |
return {
|
| 175 |
isConnected,
|
| 176 |
connectionState,
|
| 177 |
manualReconnect,
|
| 178 |
+
stopCurrentTask,
|
| 179 |
};
|
| 180 |
};
|
cua2-front/src/hooks/useGifGenerator.ts
CHANGED
|
@@ -44,12 +44,10 @@ export const useGifGenerator = ({
|
|
| 44 |
return;
|
| 45 |
}
|
| 46 |
|
| 47 |
-
// Generate GIF with
|
| 48 |
const options: GifGenerationOptions = {
|
| 49 |
images,
|
| 50 |
interval: 1.5, // 1.5 seconds per frame
|
| 51 |
-
gifWidth: 400,
|
| 52 |
-
gifHeight: 200,
|
| 53 |
quality: 10, // Medium quality for good size/quality compromise
|
| 54 |
};
|
| 55 |
|
|
|
|
| 44 |
return;
|
| 45 |
}
|
| 46 |
|
| 47 |
+
// Generate GIF with original image dimensions
|
| 48 |
const options: GifGenerationOptions = {
|
| 49 |
images,
|
| 50 |
interval: 1.5, // 1.5 seconds per frame
|
|
|
|
|
|
|
| 51 |
quality: 10, // Medium quality for good size/quality compromise
|
| 52 |
};
|
| 53 |
|
cua2-front/src/hooks/useJsonExporter.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
| 1 |
import { useCallback } from 'react';
|
| 2 |
import { exportTraceToJson, downloadJson } from '@/services/jsonExporter';
|
| 3 |
-
import { AgentTrace, AgentStep, AgentTraceMetadata } from '@/types/agent';
|
| 4 |
|
| 5 |
interface UseJsonExporterOptions {
|
| 6 |
trace?: AgentTrace;
|
| 7 |
steps: AgentStep[];
|
| 8 |
metadata?: AgentTraceMetadata;
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
interface UseJsonExporterReturn {
|
|
@@ -19,6 +20,7 @@ export const useJsonExporter = ({
|
|
| 19 |
trace,
|
| 20 |
steps,
|
| 21 |
metadata,
|
|
|
|
| 22 |
}: UseJsonExporterOptions): UseJsonExporterReturn => {
|
| 23 |
const downloadTraceAsJson = useCallback(() => {
|
| 24 |
if (!trace) {
|
|
@@ -27,13 +29,13 @@ export const useJsonExporter = ({
|
|
| 27 |
}
|
| 28 |
|
| 29 |
try {
|
| 30 |
-
const jsonString = exportTraceToJson(trace, steps, metadata);
|
| 31 |
const filename = `trace-${trace.id}.json`;
|
| 32 |
downloadJson(jsonString, filename);
|
| 33 |
} catch (error) {
|
| 34 |
console.error('Error exporting trace to JSON:', error);
|
| 35 |
}
|
| 36 |
-
}, [trace, steps, metadata]);
|
| 37 |
|
| 38 |
return {
|
| 39 |
downloadTraceAsJson,
|
|
|
|
| 1 |
import { useCallback } from 'react';
|
| 2 |
import { exportTraceToJson, downloadJson } from '@/services/jsonExporter';
|
| 3 |
+
import { AgentTrace, AgentStep, AgentTraceMetadata, FinalStep } from '@/types/agent';
|
| 4 |
|
| 5 |
interface UseJsonExporterOptions {
|
| 6 |
trace?: AgentTrace;
|
| 7 |
steps: AgentStep[];
|
| 8 |
metadata?: AgentTraceMetadata;
|
| 9 |
+
finalStep?: FinalStep;
|
| 10 |
}
|
| 11 |
|
| 12 |
interface UseJsonExporterReturn {
|
|
|
|
| 20 |
trace,
|
| 21 |
steps,
|
| 22 |
metadata,
|
| 23 |
+
finalStep,
|
| 24 |
}: UseJsonExporterOptions): UseJsonExporterReturn => {
|
| 25 |
const downloadTraceAsJson = useCallback(() => {
|
| 26 |
if (!trace) {
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
try {
|
| 32 |
+
const jsonString = exportTraceToJson(trace, steps, metadata, finalStep);
|
| 33 |
const filename = `trace-${trace.id}.json`;
|
| 34 |
downloadJson(jsonString, filename);
|
| 35 |
} catch (error) {
|
| 36 |
console.error('Error exporting trace to JSON:', error);
|
| 37 |
}
|
| 38 |
+
}, [trace, steps, metadata, finalStep]);
|
| 39 |
|
| 40 |
return {
|
| 41 |
downloadTraceAsJson,
|
cua2-front/src/services/gifGenerator.ts
CHANGED
|
@@ -49,32 +49,41 @@ const addStepCounter = async (
|
|
| 49 |
ctx.drawImage(img, 0, 0, width, height);
|
| 50 |
|
| 51 |
// Configure counter style
|
| 52 |
-
const fontSize = Math.max(
|
| 53 |
-
const padding = Math.max(
|
| 54 |
const text = `${stepNumber}/${totalSteps}`;
|
| 55 |
|
| 56 |
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
| 57 |
const textMetrics = ctx.measureText(text);
|
| 58 |
const textWidth = textMetrics.width;
|
| 59 |
-
const textHeight = fontSize;
|
| 60 |
-
|
| 61 |
-
// Position at bottom right
|
| 62 |
-
const x = width - textWidth - padding * 2;
|
| 63 |
-
const y = height - padding * 2;
|
| 64 |
-
|
| 65 |
-
// Draw semi-transparent rectangle for readability
|
| 66 |
-
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
| 67 |
-
ctx.fillRect(
|
| 68 |
-
x - padding,
|
| 69 |
-
y - textHeight - padding,
|
| 70 |
-
textWidth + padding * 2,
|
| 71 |
-
textHeight + padding * 2
|
| 72 |
-
);
|
| 73 |
|
| 74 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
ctx.fillStyle = '#000000';
|
| 76 |
-
ctx.
|
| 77 |
-
ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
// Convert canvas to base64
|
| 80 |
resolve(canvas.toDataURL('image/png'));
|
|
@@ -88,6 +97,28 @@ const addStepCounter = async (
|
|
| 88 |
});
|
| 89 |
};
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
/**
|
| 92 |
* Generate a GIF from a list of images (base64 or URLs)
|
| 93 |
* @param options GIF generation options
|
|
@@ -99,8 +130,8 @@ export const generateGif = async (
|
|
| 99 |
const {
|
| 100 |
images,
|
| 101 |
interval = 1.5, // 1.5 seconds per frame by default
|
| 102 |
-
gifWidth
|
| 103 |
-
gifHeight
|
| 104 |
quality = 10,
|
| 105 |
} = options;
|
| 106 |
|
|
@@ -112,10 +143,20 @@ export const generateGif = async (
|
|
| 112 |
}
|
| 113 |
|
| 114 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
// Add counter to each image
|
| 116 |
const imagesWithCounter = await Promise.all(
|
| 117 |
images.map((img, index) =>
|
| 118 |
-
addStepCounter(img, index + 1, images.length,
|
| 119 |
)
|
| 120 |
);
|
| 121 |
|
|
@@ -124,8 +165,8 @@ export const generateGif = async (
|
|
| 124 |
{
|
| 125 |
images: imagesWithCounter,
|
| 126 |
interval,
|
| 127 |
-
gifWidth,
|
| 128 |
-
gifHeight,
|
| 129 |
numFrames: imagesWithCounter.length,
|
| 130 |
frameDuration: interval,
|
| 131 |
sampleInterval: quality,
|
|
|
|
| 49 |
ctx.drawImage(img, 0, 0, width, height);
|
| 50 |
|
| 51 |
// Configure counter style
|
| 52 |
+
const fontSize = Math.max(11, Math.floor(height * 0.05));
|
| 53 |
+
const padding = Math.max(5, Math.floor(height * 0.02));
|
| 54 |
const text = `${stepNumber}/${totalSteps}`;
|
| 55 |
|
| 56 |
ctx.font = `bold ${fontSize}px Arial, sans-serif`;
|
| 57 |
const textMetrics = ctx.measureText(text);
|
| 58 |
const textWidth = textMetrics.width;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
+
// Use actual text metrics for better vertical centering
|
| 61 |
+
const actualHeight = textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent;
|
| 62 |
+
|
| 63 |
+
// Calculate box dimensions
|
| 64 |
+
const boxWidth = textWidth + padding * 2;
|
| 65 |
+
const boxHeight = actualHeight + padding * 2;
|
| 66 |
+
|
| 67 |
+
// Position at bottom right with margin
|
| 68 |
+
const margin = Math.max(8, Math.floor(height * 0.015));
|
| 69 |
+
const boxX = width - boxWidth - margin;
|
| 70 |
+
const boxY = height - boxHeight - margin;
|
| 71 |
+
|
| 72 |
+
// Draw semi-transparent rounded rectangle for readability
|
| 73 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
|
| 74 |
+
const borderRadius = 4;
|
| 75 |
+
ctx.beginPath();
|
| 76 |
+
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, borderRadius);
|
| 77 |
+
ctx.fill();
|
| 78 |
+
|
| 79 |
+
// Draw black text centered in the box
|
| 80 |
ctx.fillStyle = '#000000';
|
| 81 |
+
ctx.textAlign = 'center';
|
| 82 |
+
ctx.textBaseline = 'alphabetic';
|
| 83 |
+
// Position text precisely using actual bounding box metrics
|
| 84 |
+
const textX = boxX + boxWidth / 2;
|
| 85 |
+
const textY = boxY + padding + textMetrics.actualBoundingBoxAscent;
|
| 86 |
+
ctx.fillText(text, textX, textY);
|
| 87 |
|
| 88 |
// Convert canvas to base64
|
| 89 |
resolve(canvas.toDataURL('image/png'));
|
|
|
|
| 97 |
});
|
| 98 |
};
|
| 99 |
|
| 100 |
+
/**
|
| 101 |
+
* Get the dimensions of an image
|
| 102 |
+
* @param imageSrc Image source (base64 or URL)
|
| 103 |
+
* @returns Promise resolved with image dimensions
|
| 104 |
+
*/
|
| 105 |
+
const getImageDimensions = (imageSrc: string): Promise<{ width: number; height: number }> => {
|
| 106 |
+
return new Promise((resolve, reject) => {
|
| 107 |
+
const img = new Image();
|
| 108 |
+
img.crossOrigin = 'anonymous';
|
| 109 |
+
|
| 110 |
+
img.onload = () => {
|
| 111 |
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
img.onerror = () => {
|
| 115 |
+
reject(new Error('Failed to load image to get dimensions'));
|
| 116 |
+
};
|
| 117 |
+
|
| 118 |
+
img.src = imageSrc;
|
| 119 |
+
});
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
/**
|
| 123 |
* Generate a GIF from a list of images (base64 or URLs)
|
| 124 |
* @param options GIF generation options
|
|
|
|
| 130 |
const {
|
| 131 |
images,
|
| 132 |
interval = 1.5, // 1.5 seconds per frame by default
|
| 133 |
+
gifWidth,
|
| 134 |
+
gifHeight,
|
| 135 |
quality = 10,
|
| 136 |
} = options;
|
| 137 |
|
|
|
|
| 143 |
}
|
| 144 |
|
| 145 |
try {
|
| 146 |
+
// Get dimensions from the first image if not specified
|
| 147 |
+
let width = gifWidth;
|
| 148 |
+
let height = gifHeight;
|
| 149 |
+
|
| 150 |
+
if (!width || !height) {
|
| 151 |
+
const dimensions = await getImageDimensions(images[0]);
|
| 152 |
+
width = width || dimensions.width;
|
| 153 |
+
height = height || dimensions.height;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
// Add counter to each image
|
| 157 |
const imagesWithCounter = await Promise.all(
|
| 158 |
images.map((img, index) =>
|
| 159 |
+
addStepCounter(img, index + 1, images.length, width, height)
|
| 160 |
)
|
| 161 |
);
|
| 162 |
|
|
|
|
| 165 |
{
|
| 166 |
images: imagesWithCounter,
|
| 167 |
interval,
|
| 168 |
+
gifWidth: width,
|
| 169 |
+
gifHeight: height,
|
| 170 |
numFrames: imagesWithCounter.length,
|
| 171 |
frameDuration: interval,
|
| 172 |
sampleInterval: quality,
|
cua2-front/src/services/jsonExporter.ts
CHANGED
|
@@ -1,16 +1,46 @@
|
|
| 1 |
-
import { AgentTrace, AgentStep, AgentTraceMetadata } from '@/types/agent';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
/**
|
| 4 |
* Export the complete trace as JSON
|
| 5 |
* @param trace The agent trace
|
| 6 |
* @param steps The trace steps
|
| 7 |
* @param metadata The final metadata
|
|
|
|
| 8 |
* @returns A JSON object containing the entire trace
|
| 9 |
*/
|
| 10 |
export const exportTraceToJson = (
|
| 11 |
trace: AgentTrace,
|
| 12 |
steps: AgentStep[],
|
| 13 |
-
metadata?: AgentTraceMetadata
|
|
|
|
| 14 |
): string => {
|
| 15 |
const exportData = {
|
| 16 |
trace: {
|
|
@@ -20,6 +50,11 @@ export const exportTraceToJson = (
|
|
| 20 |
modelId: trace.modelId,
|
| 21 |
isRunning: trace.isRunning,
|
| 22 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
metadata: metadata || trace.traceMetadata,
|
| 24 |
steps: steps.map((step) => ({
|
| 25 |
traceId: step.traceId,
|
|
|
|
| 1 |
+
import { AgentTrace, AgentStep, AgentTraceMetadata, FinalStep } from '@/types/agent';
|
| 2 |
+
|
| 3 |
+
/**
|
| 4 |
+
* Extract final answer from steps
|
| 5 |
+
*/
|
| 6 |
+
const extractFinalAnswer = (steps: AgentStep[]): string | null => {
|
| 7 |
+
if (!steps || steps.length === 0) {
|
| 8 |
+
return null;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
// Try to find final_answer in any step (iterate backwards)
|
| 12 |
+
for (let i = steps.length - 1; i >= 0; i--) {
|
| 13 |
+
const step = steps[i];
|
| 14 |
+
|
| 15 |
+
if (step.actions && Array.isArray(step.actions)) {
|
| 16 |
+
const finalAnswerAction = step.actions.find(
|
| 17 |
+
(action) => action.function_name === 'final_answer'
|
| 18 |
+
);
|
| 19 |
+
|
| 20 |
+
if (finalAnswerAction && finalAnswerAction.parameters?.answer) {
|
| 21 |
+
return String(finalAnswerAction.parameters.answer);
|
| 22 |
+
}
|
| 23 |
+
}
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
// Fallback: return the last thought if no final_answer found
|
| 27 |
+
const lastStep = steps[steps.length - 1];
|
| 28 |
+
return lastStep?.thought || null;
|
| 29 |
+
};
|
| 30 |
|
| 31 |
/**
|
| 32 |
* Export the complete trace as JSON
|
| 33 |
* @param trace The agent trace
|
| 34 |
* @param steps The trace steps
|
| 35 |
* @param metadata The final metadata
|
| 36 |
+
* @param finalStep The final step with completion status
|
| 37 |
* @returns A JSON object containing the entire trace
|
| 38 |
*/
|
| 39 |
export const exportTraceToJson = (
|
| 40 |
trace: AgentTrace,
|
| 41 |
steps: AgentStep[],
|
| 42 |
+
metadata?: AgentTraceMetadata,
|
| 43 |
+
finalStep?: FinalStep
|
| 44 |
): string => {
|
| 45 |
const exportData = {
|
| 46 |
trace: {
|
|
|
|
| 50 |
modelId: trace.modelId,
|
| 51 |
isRunning: trace.isRunning,
|
| 52 |
},
|
| 53 |
+
completion: finalStep ? {
|
| 54 |
+
status: finalStep.type,
|
| 55 |
+
message: finalStep.message || null,
|
| 56 |
+
finalAnswer: extractFinalAnswer(steps),
|
| 57 |
+
} : null,
|
| 58 |
metadata: metadata || trace.traceMetadata,
|
| 59 |
steps: steps.map((step) => ({
|
| 60 |
traceId: step.traceId,
|
cua2-front/src/stores/agentStore.ts
CHANGED
|
@@ -20,7 +20,7 @@ interface AgentState {
|
|
| 20 |
// Actions
|
| 21 |
setTrace: (trace: AgentTrace | undefined) => void;
|
| 22 |
updateTraceWithStep: (step: AgentStep, metadata: AgentTraceMetadata) => void;
|
| 23 |
-
completeTrace: (metadata: AgentTraceMetadata) => void;
|
| 24 |
setIsAgentProcessing: (processing: boolean) => void;
|
| 25 |
setIsConnectingToE2B: (connecting: boolean) => void;
|
| 26 |
setVncUrl: (url: string) => void;
|
|
@@ -90,42 +90,62 @@ export const useAgentStore = create<AgentState>()(
|
|
| 90 |
'updateTraceWithStep'
|
| 91 |
),
|
| 92 |
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
},
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
// Set processing state
|
| 131 |
setIsAgentProcessing: (isAgentProcessing) =>
|
|
|
|
| 20 |
// Actions
|
| 21 |
setTrace: (trace: AgentTrace | undefined) => void;
|
| 22 |
updateTraceWithStep: (step: AgentStep, metadata: AgentTraceMetadata) => void;
|
| 23 |
+
completeTrace: (metadata: AgentTraceMetadata, finalState?: 'success' | 'stopped' | 'max_steps_reached' | 'error' | 'sandbox_timeout') => void;
|
| 24 |
setIsAgentProcessing: (processing: boolean) => void;
|
| 25 |
setIsConnectingToE2B: (connecting: boolean) => void;
|
| 26 |
setVncUrl: (url: string) => void;
|
|
|
|
| 90 |
'updateTraceWithStep'
|
| 91 |
),
|
| 92 |
|
| 93 |
+
// Complete the trace
|
| 94 |
+
completeTrace: (metadata, finalState?: 'success' | 'stopped' | 'max_steps_reached' | 'error' | 'sandbox_timeout') =>
|
| 95 |
+
set(
|
| 96 |
+
(state) => {
|
| 97 |
+
if (!state.trace) return state;
|
| 98 |
+
|
| 99 |
+
// Preserve existing maxSteps if new metadata has 0
|
| 100 |
+
const updatedMetadata = {
|
| 101 |
+
...metadata,
|
| 102 |
+
maxSteps: metadata.maxSteps > 0
|
| 103 |
+
? metadata.maxSteps
|
| 104 |
+
: (state.trace.traceMetadata?.maxSteps || 200),
|
| 105 |
+
completed: true,
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
// Determine the final step type based on final_state from backend
|
| 109 |
+
let stepType: 'success' | 'failure' | 'stopped' | 'max_steps_reached' | 'sandbox_timeout';
|
| 110 |
+
let stepMessage: string | undefined;
|
| 111 |
+
|
| 112 |
+
if (finalState === 'stopped') {
|
| 113 |
+
stepType = 'stopped';
|
| 114 |
+
stepMessage = 'Task stopped by user';
|
| 115 |
+
} else if (finalState === 'max_steps_reached') {
|
| 116 |
+
stepType = 'max_steps_reached';
|
| 117 |
+
stepMessage = 'Maximum steps reached';
|
| 118 |
+
} else if (finalState === 'sandbox_timeout') {
|
| 119 |
+
stepType = 'sandbox_timeout';
|
| 120 |
+
stepMessage = 'Sandbox timeout';
|
| 121 |
+
} else if (finalState === 'error' || state.error) {
|
| 122 |
+
stepType = 'failure';
|
| 123 |
+
stepMessage = state.error || 'Task failed';
|
| 124 |
+
} else {
|
| 125 |
+
stepType = 'success';
|
| 126 |
+
stepMessage = undefined;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
const finalStep: FinalStep = {
|
| 130 |
+
type: stepType,
|
| 131 |
+
message: stepMessage,
|
| 132 |
+
metadata: updatedMetadata,
|
| 133 |
+
};
|
| 134 |
+
|
| 135 |
+
return {
|
| 136 |
+
trace: {
|
| 137 |
+
...state.trace,
|
| 138 |
+
isRunning: false,
|
| 139 |
+
traceMetadata: updatedMetadata,
|
| 140 |
},
|
| 141 |
+
finalStep,
|
| 142 |
+
// Keep error in state for display
|
| 143 |
+
selectedStepIndex: null, // Reset to live mode on completion
|
| 144 |
+
};
|
| 145 |
+
},
|
| 146 |
+
false,
|
| 147 |
+
'completeTrace'
|
| 148 |
+
),
|
| 149 |
|
| 150 |
// Set processing state
|
| 151 |
setIsAgentProcessing: (isAgentProcessing) =>
|
cua2-front/src/types/agent.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface AgentTraceMetadata {
|
|
| 39 |
}
|
| 40 |
|
| 41 |
export interface FinalStep {
|
| 42 |
-
type: 'success' | 'failure';
|
| 43 |
message?: string;
|
| 44 |
metadata: AgentTraceMetadata;
|
| 45 |
}
|
|
|
|
| 39 |
}
|
| 40 |
|
| 41 |
export interface FinalStep {
|
| 42 |
+
type: 'success' | 'failure' | 'stopped' | 'max_steps_reached' | 'sandbox_timeout';
|
| 43 |
message?: string;
|
| 44 |
metadata: AgentTraceMetadata;
|
| 45 |
}
|