tfrere HF Staff commited on
Commit
09644e5
·
1 Parent(s): 67b042f

Front update 2 (#6)

Browse files

* Add Git LFS for PNG files

* Remove simulation images (not needed for deployment)

* Add HF Space metadata to README

* Add HF Space YAML metadata to README

* Translate all French text to English in frontend

- Translate comments in gifGenerator.ts
- Translate comments in jsonExporter.ts
- Translate comments in useJsonExporter.ts
- Translate error messages to English

* Translate remaining French comments to English in frontend

* Translate last French comments in StepCard.tsx

.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  # CUA2 - Computer Use Agent 2
2
 
3
  An AI-powered automation interface featuring real-time agent task processing, VNC streaming, and step-by-step execution visualization.
 
1
+ ---
2
+ title: CUA2 - Computer Use Agent 2
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ pinned: false
8
+ ---
9
+
10
  # CUA2 - Computer Use Agent 2
11
 
12
  An AI-powered automation interface featuring real-time agent task processing, VNC streaming, and step-by-step execution visualization.
assets/architecture.png CHANGED

Git LFS Details

  • SHA256: 4154d6473e27f9f304c48de27b34c44f6605d9bbcbcb00adcad0722153c50b96
  • Pointer size: 130 Bytes
  • Size of remote file: 39.1 kB
cua2-front/src/components/steps/ConnectionStepCard.tsx CHANGED
@@ -3,7 +3,7 @@ import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/mater
3
  import CableIcon from '@mui/icons-material/Cable';
4
  import { keyframes } from '@mui/system';
5
 
6
- // Animation de pulsation pour le border
7
  const borderPulse = keyframes`
8
  0%, 100% {
9
  border-color: rgba(79, 134, 198, 0.4);
@@ -15,7 +15,7 @@ const borderPulse = keyframes`
15
  }
16
  `;
17
 
18
- // Animation de pulsation pour le fond
19
  const backgroundPulse = keyframes`
20
  0%, 100% {
21
  background-color: rgba(79, 134, 198, 0.03);
@@ -54,7 +54,7 @@ export const ConnectionStepCard: React.FC<ConnectionStepCardProps> = ({ isConnec
54
  }}
55
  >
56
  <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 }, position: 'relative', zIndex: 1 }}>
57
- {/* Header avec spinner ou check */}
58
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
59
  <Box
60
  sx={{
 
3
  import CableIcon from '@mui/icons-material/Cable';
4
  import { keyframes } from '@mui/system';
5
 
6
+ // Border pulse animation
7
  const borderPulse = keyframes`
8
  0%, 100% {
9
  border-color: rgba(79, 134, 198, 0.4);
 
15
  }
16
  `;
17
 
18
+ // Background pulse animation
19
  const backgroundPulse = keyframes`
20
  0%, 100% {
21
  background-color: rgba(79, 134, 198, 0.03);
 
54
  }}
55
  >
56
  <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 }, position: 'relative', zIndex: 1 }}>
57
+ {/* Header with spinner or check */}
58
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
59
  <Box
60
  sx={{
cua2-front/src/components/steps/StepCard.tsx CHANGED
@@ -30,11 +30,11 @@ export const StepCard: React.FC<StepCardProps> = ({ step, index, isLatest = fals
30
  };
31
 
32
  const handleAccordionClick = (event: React.MouseEvent) => {
33
- event.stopPropagation(); // Empêcher la propagation pour ne pas sélectionner la step
34
  };
35
 
36
  const handleVote = async (event: React.MouseEvent, vote: 'like' | 'dislike') => {
37
- event.stopPropagation(); // Empêcher la propagation pour ne pas sélectionner la step
38
 
39
  if (isVoting) return;
40
 
 
30
  };
31
 
32
  const handleAccordionClick = (event: React.MouseEvent) => {
33
+ event.stopPropagation(); // Prevent propagation to avoid selecting the step
34
  };
35
 
36
  const handleVote = async (event: React.MouseEvent, vote: 'like' | 'dislike') => {
37
+ event.stopPropagation(); // Prevent propagation to avoid selecting the step
38
 
39
  if (isVoting) return;
40
 
cua2-front/src/components/steps/StepsList.tsx CHANGED
@@ -59,13 +59,13 @@ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
59
  // - Remains visible during the entire agent processing
60
  // - Hides only when agent stops OR a finalStep exists
61
  useEffect(() => {
62
- // Si le stream démarre vraiment (isAgentProcessing = true et PAS en train de se connecter)
63
- // Et pas encore de startTime enregistré
64
  if (isAgentProcessing && !isConnectingToE2B && !streamStartTimeRef.current) {
65
  streamStartTimeRef.current = Date.now();
66
  }
67
 
68
- // Si l'agent s'arrête OU qu'on a un finalStep, reset et cacher
69
  if (!isAgentProcessing || finalStep) {
70
  streamStartTimeRef.current = null;
71
  setShowThinkingCard(false);
@@ -83,7 +83,7 @@ export const StepsList: React.FC<StepsListProps> = ({ trace }) => {
83
  clearTimeout(thinkingTimeoutRef.current);
84
  }
85
 
86
- // Calculer le temps écoulé depuis le début du stream
87
  const elapsedTime = Date.now() - streamStartTimeRef.current;
88
  const remainingTime = Math.max(0, 5000 - elapsedTime);
89
 
 
59
  // - Remains visible during the entire agent processing
60
  // - Hides only when agent stops OR a finalStep exists
61
  useEffect(() => {
62
+ // If stream really starts (isAgentProcessing = true and NOT connecting)
63
+ // And no startTime recorded yet
64
  if (isAgentProcessing && !isConnectingToE2B && !streamStartTimeRef.current) {
65
  streamStartTimeRef.current = Date.now();
66
  }
67
 
68
+ // If agent stops OR we have a finalStep, reset and hide
69
  if (!isAgentProcessing || finalStep) {
70
  streamStartTimeRef.current = null;
71
  setShowThinkingCard(false);
 
83
  clearTimeout(thinkingTimeoutRef.current);
84
  }
85
 
86
+ // Calculate elapsed time since stream started
87
  const elapsedTime = Date.now() - streamStartTimeRef.current;
88
  const remainingTime = Math.max(0, 5000 - elapsedTime);
89
 
cua2-front/src/components/steps/ThinkingStepCard.tsx CHANGED
@@ -2,7 +2,7 @@ import React from 'react';
2
  import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/material';
3
  import { keyframes } from '@mui/system';
4
 
5
- // Animation de pulsation pour le border
6
  const borderPulse = keyframes`
7
  0%, 100% {
8
  border-color: rgba(79, 134, 198, 0.4);
@@ -14,7 +14,7 @@ const borderPulse = keyframes`
14
  }
15
  `;
16
 
17
- // Animation de pulsation pour le fond
18
  const backgroundPulse = keyframes`
19
  0%, 100% {
20
  background-color: rgba(79, 134, 198, 0.03);
@@ -50,7 +50,7 @@ export const ThinkingStepCard: React.FC = () => {
50
  }}
51
  >
52
  <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 }, position: 'relative', zIndex: 1 }}>
53
- {/* Header avec spinner */}
54
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
55
  <Box
56
  sx={{
 
2
  import { Card, CardContent, Box, Typography, CircularProgress } from '@mui/material';
3
  import { keyframes } from '@mui/system';
4
 
5
+ // Border pulse animation
6
  const borderPulse = keyframes`
7
  0%, 100% {
8
  border-color: rgba(79, 134, 198, 0.4);
 
14
  }
15
  `;
16
 
17
+ // Background pulse animation
18
  const backgroundPulse = keyframes`
19
  0%, 100% {
20
  background-color: rgba(79, 134, 198, 0.03);
 
50
  }}
51
  >
52
  <CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 }, position: 'relative', zIndex: 1 }}>
53
+ {/* Header with spinner */}
54
  <Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
55
  <Box
56
  sx={{
cua2-front/src/components/timeline/Timeline.tsx CHANGED
@@ -131,7 +131,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
131
  content: '""',
132
  position: 'absolute',
133
  left: "15px",
134
- // Calculer la largeur pour couvrir tous les steps (200 steps * (40px minWidth + 12px gap))
135
  width: `calc(${metadata.maxSteps} * (40px + 12px))`,
136
  top: '17.5px',
137
  transform: 'translateY(-50%)',
@@ -157,7 +157,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
157
  zIndex: 1,
158
  }}
159
  >
160
- {/* Cercle blanc en arrière-plan pour cacher la ligne */}
161
  <Box
162
  sx={{
163
  position: 'relative',
@@ -166,7 +166,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
166
  justifyContent: 'center',
167
  }}
168
  >
169
- {/* Fond blanc/background pour cacher la ligne */}
170
  <Box
171
  sx={{
172
  position: 'absolute',
@@ -239,7 +239,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
239
  } : {},
240
  }}
241
  >
242
- {/* Cercle blanc en arrière-plan pour cacher la ligne */}
243
  <Box
244
  sx={{
245
  position: 'relative',
@@ -248,7 +248,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
248
  justifyContent: 'center',
249
  }}
250
  >
251
- {/* Fond blanc/background pour cacher la ligne */}
252
  <Box
253
  sx={{
254
  position: 'absolute',
@@ -338,7 +338,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
338
  },
339
  }}
340
  >
341
- {/* Cercle blanc en arrière-plan pour cacher la ligne */}
342
  <Box
343
  sx={{
344
  position: 'relative',
@@ -347,7 +347,7 @@ export const Timeline: React.FC<TimelineProps> = ({ metadata, isRunning }) => {
347
  justifyContent: 'center',
348
  }}
349
  >
350
- {/* Fond blanc/background pour cacher la ligne */}
351
  <Box
352
  sx={{
353
  position: 'absolute',
 
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%)',
 
157
  zIndex: 1,
158
  }}
159
  >
160
+ {/* White circle background to hide the line */}
161
  <Box
162
  sx={{
163
  position: 'relative',
 
166
  justifyContent: 'center',
167
  }}
168
  >
169
+ {/* White background to hide the line */}
170
  <Box
171
  sx={{
172
  position: 'absolute',
 
239
  } : {},
240
  }}
241
  >
242
+ {/* White circle background to hide the line */}
243
  <Box
244
  sx={{
245
  position: 'relative',
 
248
  justifyContent: 'center',
249
  }}
250
  >
251
+ {/* White background to hide the line */}
252
  <Box
253
  sx={{
254
  position: 'absolute',
 
338
  },
339
  }}
340
  >
341
+ {/* White circle background to hide the line */}
342
  <Box
343
  sx={{
344
  position: 'relative',
 
347
  justifyContent: 'center',
348
  }}
349
  >
350
+ {/* White background to hide the line */}
351
  <Box
352
  sx={{
353
  position: 'absolute',
cua2-front/src/hooks/useJsonExporter.ts CHANGED
@@ -13,7 +13,7 @@ interface UseJsonExporterReturn {
13
  }
14
 
15
  /**
16
- * Hook personnalisé pour exporter et télécharger une trace en JSON
17
  */
18
  export const useJsonExporter = ({
19
  trace,
 
13
  }
14
 
15
  /**
16
+ * Custom hook to export and download a trace as JSON
17
  */
18
  export const useJsonExporter = ({
19
  trace,
cua2-front/src/services/gifGenerator.ts CHANGED
@@ -2,7 +2,7 @@ import gifshot from 'gifshot';
2
 
3
  export interface GifGenerationOptions {
4
  images: string[];
5
- interval?: number; // Durée de chaque frame en secondes
6
  gifWidth?: number;
7
  gifHeight?: number;
8
  quality?: number;
@@ -10,18 +10,18 @@ export interface GifGenerationOptions {
10
 
11
  export interface GifGenerationResult {
12
  success: boolean;
13
- image?: string; // Data URL du GIF
14
  error?: string;
15
  }
16
 
17
  /**
18
- * Ajoute un compteur d'étapes sur une image
19
- * @param imageSrc Source de l'image (base64 ou URL)
20
- * @param stepNumber Numéro de l'étape
21
- * @param totalSteps Nombre total d'étapes
22
- * @param width Largeur de l'image
23
- * @param height Hauteur de l'image
24
- * @returns Promesse résolue avec l'image modifiée en base64
25
  */
26
  const addStepCounter = async (
27
  imageSrc: string,
@@ -45,10 +45,10 @@ const addStepCounter = async (
45
  return;
46
  }
47
 
48
- // Dessiner l'image
49
  ctx.drawImage(img, 0, 0, width, height);
50
 
51
- // Configurer le style du compteur
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}`;
@@ -58,11 +58,11 @@ const addStepCounter = async (
58
  const textWidth = textMetrics.width;
59
  const textHeight = fontSize;
60
 
61
- // Position en bas à droite
62
  const x = width - textWidth - padding * 2;
63
  const y = height - padding * 2;
64
 
65
- // Dessiner un rectangle semi-transparent pour la lisibilité
66
  ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
67
  ctx.fillRect(
68
  x - padding,
@@ -71,12 +71,12 @@ const addStepCounter = async (
71
  textHeight + padding * 2
72
  );
73
 
74
- // Dessiner le texte en noir
75
  ctx.fillStyle = '#000000';
76
  ctx.textBaseline = 'top';
77
  ctx.fillText(text, x, y - textHeight);
78
 
79
- // Convertir le canvas en base64
80
  resolve(canvas.toDataURL('image/png'));
81
  };
82
 
@@ -89,16 +89,16 @@ const addStepCounter = async (
89
  };
90
 
91
  /**
92
- * Génère un GIF à partir d'une liste d'images (base64 ou URLs)
93
- * @param options Options de génération du GIF
94
- * @returns Promesse résolue avec le résultat de la génération
95
  */
96
  export const generateGif = async (
97
  options: GifGenerationOptions
98
  ): Promise<GifGenerationResult> => {
99
  const {
100
  images,
101
- interval = 1.5, // 1.5 secondes par frame par défaut
102
  gifWidth = 400,
103
  gifHeight = 200,
104
  quality = 10,
@@ -107,12 +107,12 @@ export const generateGif = async (
107
  if (!images || images.length === 0) {
108
  return {
109
  success: false,
110
- error: 'Aucune image fournie pour générer le GIF',
111
  };
112
  }
113
 
114
  try {
115
- // Ajouter le compteur sur chaque image
116
  const imagesWithCounter = await Promise.all(
117
  images.map((img, index) =>
118
  addStepCounter(img, index + 1, images.length, gifWidth, gifHeight)
@@ -134,7 +134,7 @@ export const generateGif = async (
134
  if (obj.error) {
135
  resolve({
136
  success: false,
137
- error: obj.errorMsg || 'Erreur lors de la génération du GIF',
138
  });
139
  } else {
140
  resolve({
@@ -148,15 +148,15 @@ export const generateGif = async (
148
  } catch (error) {
149
  return {
150
  success: false,
151
- error: error instanceof Error ? error.message : 'Erreur inconnue',
152
  };
153
  }
154
  };
155
 
156
  /**
157
- * Télécharge un GIF (data URL) avec un nom de fichier
158
- * @param dataUrl Data URL du GIF
159
- * @param filename Nom du fichier à télécharger
160
  */
161
  export const downloadGif = (dataUrl: string, filename: string = 'trace-replay.gif') => {
162
  const link = document.createElement('a');
 
2
 
3
  export interface GifGenerationOptions {
4
  images: string[];
5
+ interval?: number; // Duration of each frame in seconds
6
  gifWidth?: number;
7
  gifHeight?: number;
8
  quality?: number;
 
10
 
11
  export interface GifGenerationResult {
12
  success: boolean;
13
+ image?: string; // GIF data URL
14
  error?: string;
15
  }
16
 
17
  /**
18
+ * Add step counter to an image
19
+ * @param imageSrc Image source (base64 or URL)
20
+ * @param stepNumber Step number
21
+ * @param totalSteps Total number of steps
22
+ * @param width Image width
23
+ * @param height Image height
24
+ * @returns Promise resolved with modified image in base64
25
  */
26
  const addStepCounter = async (
27
  imageSrc: string,
 
45
  return;
46
  }
47
 
48
+ // Draw the image
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}`;
 
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,
 
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'));
81
  };
82
 
 
89
  };
90
 
91
  /**
92
+ * Generate a GIF from a list of images (base64 or URLs)
93
+ * @param options GIF generation options
94
+ * @returns Promise resolved with generation result
95
  */
96
  export const generateGif = async (
97
  options: GifGenerationOptions
98
  ): Promise<GifGenerationResult> => {
99
  const {
100
  images,
101
+ interval = 1.5, // 1.5 seconds per frame by default
102
  gifWidth = 400,
103
  gifHeight = 200,
104
  quality = 10,
 
107
  if (!images || images.length === 0) {
108
  return {
109
  success: false,
110
+ error: 'No images provided to generate GIF',
111
  };
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)
 
134
  if (obj.error) {
135
  resolve({
136
  success: false,
137
+ error: obj.errorMsg || 'Error during GIF generation',
138
  });
139
  } else {
140
  resolve({
 
148
  } catch (error) {
149
  return {
150
  success: false,
151
+ error: error instanceof Error ? error.message : 'Unknown error',
152
  };
153
  }
154
  };
155
 
156
  /**
157
+ * Download a GIF (data URL) with a filename
158
+ * @param dataUrl GIF data URL
159
+ * @param filename Filename to download
160
  */
161
  export const downloadGif = (dataUrl: string, filename: string = 'trace-replay.gif') => {
162
  const link = document.createElement('a');
cua2-front/src/services/jsonExporter.ts CHANGED
@@ -31,7 +31,7 @@ export const exportTraceToJson = (
31
  inputTokensUsed: step.inputTokensUsed,
32
  outputTokensUsed: step.outputTokensUsed,
33
  step_evaluation: step.step_evaluation,
34
- // Ne pas inclure l'image base64 pour réduire la taille du JSON
35
  hasImage: !!step.image,
36
  })),
37
  exportedAt: new Date().toISOString(),
@@ -41,9 +41,9 @@ export const exportTraceToJson = (
41
  };
42
 
43
  /**
44
- * Télécharge un JSON avec un nom de fichier
45
- * @param jsonString String JSON à télécharger
46
- * @param filename Nom du fichier à télécharger
47
  */
48
  export const downloadJson = (jsonString: string, filename: string = 'trace.json') => {
49
  const blob = new Blob([jsonString], { type: 'application/json' });
 
31
  inputTokensUsed: step.inputTokensUsed,
32
  outputTokensUsed: step.outputTokensUsed,
33
  step_evaluation: step.step_evaluation,
34
+ // Don't include base64 image to reduce JSON size
35
  hasImage: !!step.image,
36
  })),
37
  exportedAt: new Date().toISOString(),
 
41
  };
42
 
43
  /**
44
+ * Download a JSON with a filename
45
+ * @param jsonString JSON string to download
46
+ * @param filename Filename to download
47
  */
48
  export const downloadJson = (jsonString: string, filename: string = 'trace.json') => {
49
  const blob = new Blob([jsonString], { type: 'application/json' });