tfrere HF Staff commited on
Commit
26f5120
·
unverified ·
1 Parent(s): 28b1bb6

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 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
- useAgentWebSocket({ url: getWebSocketUrl() });
 
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
- if (finalStep.type === 'failure') {
129
- return { label: 'Task failed', color: 'error', icon: <CloseIcon sx={{ fontSize: 16, color: 'error.main' }} /> };
 
 
 
 
 
 
 
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 PlayArrowIcon from '@mui/icons-material/PlayArrow';
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={<PlayArrowIcon sx={{ fontSize: 20 }} />}
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 isSuccess = finalStep.type === 'success';
44
- const statusColor = isSuccess ? 'success.main' : 'error.main';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: statusColor,
73
  display: 'flex',
74
  alignItems: 'center',
75
  justifyContent: 'center',
76
- boxShadow: (theme) =>
77
- isSuccess
78
- ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.3)' : 'rgba(102, 187, 106, 0.2)'}`
79
- : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.3)' : 'rgba(244, 67, 54, 0.2)'}`,
 
 
 
 
80
  }}
81
  >
82
- {isSuccess ? (
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: statusColor,
93
  fontSize: '1.1rem',
94
  letterSpacing: '-0.5px',
95
  }}
96
  >
97
- {isSuccess ? 'Task Completed' : 'Task Failed'}
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 isSuccess = finalStep.type === 'success';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ? isSuccess ? theme.palette.success.main : theme.palette.error.main
32
  : theme.palette.divider} !important`,
33
  borderRadius: 1.5,
34
  transition: 'all 0.2s ease',
35
  cursor: 'pointer',
36
  boxShadow: isActive
37
- ? (theme) => isSuccess
38
- ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.3)' : 'rgba(102, 187, 106, 0.2)'}`
39
- : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.3)' : 'rgba(244, 67, 54, 0.2)'}`
40
  : 'none',
41
  '&:hover': {
42
- borderColor: (theme) => `${isSuccess ? theme.palette.success.main : theme.palette.error.main} !important`,
43
- boxShadow: (theme) => isSuccess
44
- ? `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.2)' : 'rgba(102, 187, 106, 0.1)'}`
45
- : `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.2)' : 'rgba(244, 67, 54, 0.1)'}`,
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
- {isSuccess ? (
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: isSuccess ? 'success.main' : 'error.main',
62
  }}
63
  >
64
- {isSuccess ? 'Task completed' : 'Task failed'}
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={{ mt: 1.5, p: 1, borderRadius: 1, backgroundColor: 'error.main', opacity: 0.1 }}>
 
 
 
 
 
 
 
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
- : (trace?.steps && trace.steps.length > 0 && trace?.isRunning)
42
- ? trace.steps.length - 1
43
- : (trace?.steps && trace.steps.length > 0)
44
  ? trace.steps.length - 1
45
- : null;
 
 
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 to active step when it changes (timeline → steps)
105
  useEffect(() => {
106
- if (containerRef.current) {
107
- isScrollingProgrammatically.current = true;
108
- // Use setTimeout to ensure DOM has updated
109
- setTimeout(() => {
110
- if (containerRef.current) {
111
- // Scroll to final step if it's active
112
- if (isFinalStepActive) {
113
- const finalStepElement = containerRef.current.querySelector(`[data-step-index="final"]`);
114
- if (finalStepElement) {
115
- finalStepElement.scrollIntoView({
116
- behavior: 'smooth',
117
- block: 'center'
118
- });
119
- setTimeout(() => {
120
- isScrollingProgrammatically.current = false;
121
- }, 500);
122
- }
123
- }
124
- // Otherwise scroll to active step
125
- else if (activeStepIndex !== null && trace?.steps) {
126
- const activeStepElement = containerRef.current.querySelector(`[data-step-index="${activeStepIndex}"]`);
127
- if (activeStepElement) {
128
- activeStepElement.scrollIntoView({
129
- behavior: 'smooth',
130
- block: 'center'
131
- });
132
- // Reset flag after scroll animation
133
- setTimeout(() => {
134
- isScrollingProgrammatically.current = false;
135
- }, 500);
136
- }
137
- }
138
  }
139
- }, 100);
140
- }
141
- }, [activeStepIndex, trace?.steps?.length, isFinalStepActive]);
 
 
 
 
 
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
- export const ThinkingStepCard: React.FC = () => {
 
 
 
 
28
 
29
  return (
30
  <Card
31
  elevation={0}
32
  sx={{
33
  backgroundColor: 'background.paper',
34
- border: '2px solid',
35
- borderColor: 'primary.main',
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
- // Show all steps up to maxSteps (200)
27
- const totalStepsToShow = metadata.maxSteps;
 
 
 
 
 
 
 
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: "15px",
134
- // Calculate width to cover all steps (200 steps * (40px minWidth + 12px gap))
135
- width: `calc(${metadata.maxSteps} * (40px + 12px))`,
136
- top: '17.5px',
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.1)',
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: 50,
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: 32,
174
- height: 32,
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: step.isCurrent || step.isSelected ? 28 : step.isCompleted ? 22 : 20,
256
- height: step.isCurrent || step.isSelected ? 28 : step.isCompleted ? 22 : 20,
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
- <CircularProgress
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
- width: step.isSelected ? 20 : step.isCompleted ? 14 : 12,
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.isCompleted || step.isCurrent || step.isSelected ? 700 : 400,
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: 50,
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: selectedStepIndex === null ? 32 : 28,
355
- height: selectedStepIndex === null ? 32 : 28,
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 ? 24 : 20,
367
- height: selectedStepIndex === null ? 24 : 20,
368
  borderRadius: '50%',
369
- backgroundColor: finalStep.type === 'success' ? 'success.main' : 'error.main',
 
 
 
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
- : '0 2px 8px rgba(244, 67, 54, 0.4)'
 
 
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: finalStep.type === 'success'
398
- ? (selectedStepIndex === null ? 'text.primary' : 'text.secondary')
399
- : 'error.main',
 
 
 
400
  whiteSpace: 'nowrap',
401
  }}
402
  >
403
- {finalStep.type === 'success' ? 'End' : 'Failed'}
 
 
 
 
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 maximum dimensions of 400x200
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(12, Math.floor(height * 0.08));
53
- const padding = Math.max(6, Math.floor(height * 0.03));
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
- // Draw black text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  ctx.fillStyle = '#000000';
76
- ctx.textBaseline = 'top';
77
- ctx.fillText(text, x, y - textHeight);
 
 
 
 
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 = 400,
103
- gifHeight = 200,
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, gifWidth, gifHeight)
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
- // Complete the trace
94
- completeTrace: (metadata) =>
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 if the task succeeded or failed based on error state
109
- const finalStep: FinalStep = {
110
- type: state.error ? 'failure' : 'success',
111
- message: state.error,
112
- metadata: updatedMetadata,
113
- };
114
-
115
- return {
116
- trace: {
117
- ...state.trace,
118
- isRunning: false,
119
- traceMetadata: updatedMetadata,
120
- },
121
- finalStep,
122
- // Keep error in state for display
123
- selectedStepIndex: null, // Reset to live mode on completion
124
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  },
126
- false,
127
- 'completeTrace'
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
  }