| import { useTraceUploader } from '@/hooks/useTraceUploader';
|
| import { useAgentStore } from '@/stores/agentStore';
|
| import { AgentStep, AgentTrace, AgentTraceMetadata, FinalStep } from '@/types/agent';
|
| import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
| import AddIcon from '@mui/icons-material/Add';
|
| import AssignmentIcon from '@mui/icons-material/Assignment';
|
| import ChatBubbleOutlineIcon from '@mui/icons-material/ChatBubbleOutline';
|
| import CheckIcon from '@mui/icons-material/Check';
|
| import CloseIcon from '@mui/icons-material/Close';
|
| import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
| import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
|
| import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
| import InputIcon from '@mui/icons-material/Input';
|
| import OutputIcon from '@mui/icons-material/Output';
|
| import SmartToyIcon from '@mui/icons-material/SmartToy';
|
| import StopCircleIcon from '@mui/icons-material/StopCircle';
|
| import ThumbDownIcon from '@mui/icons-material/ThumbDown';
|
| import ThumbUpIcon from '@mui/icons-material/ThumbUp';
|
| import { Alert, Box, Button, Divider, IconButton, Paper, Tooltip, Typography } from '@mui/material';
|
| import React, { useEffect, useState, useRef, useCallback } from 'react';
|
| import { DownloadGifButton } from './DownloadGifButton';
|
| import { DownloadJsonButton } from './DownloadJsonButton';
|
|
|
| interface CompletionViewProps {
|
| finalStep: FinalStep;
|
| trace?: AgentTrace;
|
| steps?: AgentStep[];
|
| metadata?: AgentTraceMetadata;
|
| finalAnswer?: string | null;
|
| isGenerating: boolean;
|
| gifError: string | null;
|
| onGenerateGif: () => void;
|
| onDownloadJson: () => void;
|
| onBackToHome: () => void;
|
| }
|
|
|
| |
| |
|
|
| export const CompletionView: React.FC<CompletionViewProps> = ({
|
| finalStep,
|
| trace,
|
| steps,
|
| metadata,
|
| finalAnswer,
|
| isGenerating,
|
| gifError,
|
| onGenerateGif,
|
| onDownloadJson,
|
| onBackToHome,
|
| }) => {
|
| const updateTraceEvaluationInStore = useAgentStore((state) => state.updateTraceEvaluation);
|
| const [evaluation, setEvaluation] = useState<'success' | 'failed' | 'not_evaluated'>(
|
| finalStep.metadata.user_evaluation || 'not_evaluated'
|
| );
|
| const [isVoting, setIsVoting] = useState(false);
|
|
|
|
|
| const traceRef = useRef(trace);
|
| const stepsRef = useRef(steps || []);
|
| const metadataRef = useRef(metadata || finalStep.metadata);
|
| const finalStepRef = useRef(finalStep);
|
|
|
|
|
| useEffect(() => {
|
| traceRef.current = trace;
|
| stepsRef.current = steps || [];
|
| metadataRef.current = metadata || finalStep.metadata;
|
| finalStepRef.current = finalStep;
|
| }, [trace, steps, metadata, finalStep]);
|
|
|
|
|
| const { uploadTrace, isUploading, uploadError, uploadSuccess } = useTraceUploader({
|
| getTraceData: useCallback(() => ({
|
| trace: traceRef.current,
|
| steps: stepsRef.current,
|
| metadata: metadataRef.current,
|
| finalStep: finalStepRef.current,
|
| }), []),
|
| });
|
|
|
|
|
|
|
|
|
| const handleTraceEvaluation = async (vote: 'success' | 'failed') => {
|
| if (isVoting || !trace?.id) return;
|
|
|
| const newEvaluation = evaluation === vote ? 'not_evaluated' : vote;
|
| setIsVoting(true);
|
|
|
| try {
|
| setEvaluation(newEvaluation);
|
|
|
| updateTraceEvaluationInStore(newEvaluation);
|
|
|
|
|
|
|
|
|
| setTimeout(() => {
|
| uploadTrace(true);
|
| }, 100);
|
| } catch (error) {
|
| console.error('Failed to update trace evaluation:', error);
|
| } finally {
|
| setIsVoting(false);
|
| }
|
| };
|
|
|
| const getStatusConfig = () => {
|
| switch (finalStep.type) {
|
| case 'success':
|
| return {
|
| icon: <CheckIcon sx={{ fontSize: 28 }} />,
|
| title: 'Task Completed Successfully!',
|
| color: 'success.main',
|
| };
|
| case 'stopped':
|
| return {
|
| icon: <StopCircleIcon sx={{ fontSize: 28 }} />,
|
| title: 'Task Stopped',
|
| color: 'warning.main',
|
| };
|
| case 'max_steps_reached':
|
| return {
|
| icon: <HourglassEmptyIcon sx={{ fontSize: 28 }} />,
|
| title: 'Maximum Steps Reached',
|
| color: 'warning.main',
|
| };
|
| case 'sandbox_timeout':
|
| return {
|
| icon: <AccessTimeIcon sx={{ fontSize: 28 }} />,
|
| title: 'Max Sandbox Time Reached',
|
| color: 'error.main',
|
| };
|
| case 'failure':
|
| default:
|
| return {
|
| icon: <CloseIcon sx={{ fontSize: 28 }} />,
|
| title: 'Task Failed (Agent Internal Error)',
|
| color: 'error.main',
|
| };
|
| }
|
| };
|
|
|
| const statusConfig = getStatusConfig();
|
|
|
|
|
| const formatModelName = (modelId: string) => {
|
| const parts = modelId.split('/');
|
| return parts.length > 1 ? parts[1] : modelId;
|
| };
|
|
|
| return (
|
| <Box
|
| sx={{
|
| width: '100%',
|
| maxWidth: 600,
|
| mx: 'auto',
|
| p: 2,
|
| display: 'flex',
|
| flexDirection: 'column',
|
| gap: 1.5,
|
| }}
|
| >
|
| {/* Status Header - Compact */}
|
| <Box sx={{ textAlign: 'center', mb: 0.5 }}>
|
| <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1.5, mb: 0.75 }}>
|
| <Box
|
| sx={{
|
| width: 40,
|
| height: 40,
|
| borderRadius: '50%',
|
| backgroundColor: statusConfig.color,
|
| display: 'flex',
|
| alignItems: 'center',
|
| justifyContent: 'center',
|
| boxShadow: (theme) => {
|
| const rgba = finalStep.type === 'success'
|
| ? '102, 187, 106'
|
| : (finalStep.type === 'failure' || finalStep.type === 'sandbox_timeout')
|
| ? '244, 67, 54'
|
| : '255, 152, 0';
|
| return `0 2px 8px ${theme.palette.mode === 'dark' ? `rgba(${rgba}, 0.3)` : `rgba(${rgba}, 0.2)`}`;
|
| },
|
| }}
|
| >
|
| {React.cloneElement(statusConfig.icon, { sx: { fontSize: 24, color: 'white' } })}
|
| </Box>
|
| <Typography
|
| variant="h6"
|
| sx={{
|
| fontWeight: 700,
|
| color: statusConfig.color,
|
| fontSize: '1.1rem',
|
| letterSpacing: '-0.5px',
|
| }}
|
| >
|
| {statusConfig.title}
|
| </Typography>
|
| </Box>
|
| </Box>
|
|
|
| {/* Single Report Box - Task + Agent + Response + Metrics */}
|
| <Paper
|
| elevation={0}
|
| sx={{
|
| p: 2.5,
|
| backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.03)',
|
| borderRadius: 1.5,
|
| border: '1px solid',
|
| borderColor: 'divider',
|
| }}
|
| >
|
| {/* Task */}
|
| {trace?.instruction && (
|
| <Box sx={{ mb: 2 }}>
|
| <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
|
| <AssignmentIcon sx={{ fontSize: 18, color: 'text.secondary', mt: 0.25, flexShrink: 0 }} />
|
| <Box sx={{ flex: 1, minWidth: 0 }}>
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontWeight: 700,
|
| color: 'text.secondary',
|
| fontSize: '0.7rem',
|
| textTransform: 'uppercase',
|
| letterSpacing: '0.5px',
|
| display: 'block',
|
| mb: 0.5,
|
| }}
|
| >
|
| Task
|
| </Typography>
|
| <Typography
|
| variant="body2"
|
| sx={{
|
| color: 'text.primary',
|
| fontWeight: 700,
|
| lineHeight: 1.5,
|
| fontSize: '0.85rem',
|
| }}
|
| >
|
| {trace.instruction}
|
| </Typography>
|
| </Box>
|
| </Box>
|
| </Box>
|
| )}
|
|
|
| {/* Agent Response */}
|
| {finalAnswer && (
|
| <Box sx={{ mb: 2 }}>
|
| <Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
|
| <ChatBubbleOutlineIcon
|
| sx={{
|
| fontSize: 18,
|
| color: 'text.secondary',
|
| mt: 0.25,
|
| flexShrink: 0
|
| }}
|
| />
|
| <Box sx={{ flex: 1, minWidth: 0 }}>
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontWeight: 700,
|
| color: 'text.secondary',
|
| fontSize: '0.7rem',
|
| textTransform: 'uppercase',
|
| letterSpacing: '0.5px',
|
| display: 'block',
|
| mb: 0.75,
|
| }}
|
| >
|
| Agent Response
|
| </Typography>
|
| <Typography
|
| variant="body2"
|
| sx={{
|
| color: 'text.primary',
|
| lineHeight: 1.5,
|
| fontSize: '0.85rem',
|
| whiteSpace: 'pre-wrap',
|
| wordBreak: 'break-word',
|
| }}
|
| >
|
| {finalAnswer}
|
| </Typography>
|
| </Box>
|
| </Box>
|
| </Box>
|
| )}
|
|
|
| {/* Trace Evaluation */}
|
| <Box sx={{ mb: 2 }}>
|
| <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontWeight: 700,
|
| color: 'text.secondary',
|
| fontSize: '0.7rem',
|
| textTransform: 'uppercase',
|
| letterSpacing: '0.5px',
|
| }}
|
| >
|
| Was this task completed successfully?
|
| </Typography>
|
|
|
| {/* Evaluation buttons */}
|
| <Box sx={{ display: 'flex', gap: 1 }}>
|
| <Tooltip title={evaluation === 'success' ? 'Remove success rating' : 'Mark as successful'}>
|
| <IconButton
|
| size="small"
|
| onClick={() => handleTraceEvaluation('success')}
|
| disabled={isVoting}
|
| sx={{
|
| padding: '4px',
|
| color: evaluation === 'success' ? 'success.main' : 'action.disabled',
|
| '&:hover': {
|
| color: 'success.main',
|
| backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(102, 187, 106, 0.1)' : 'rgba(102, 187, 106, 0.08)',
|
| },
|
| }}
|
| >
|
| <ThumbUpIcon sx={{ fontSize: 18 }} />
|
| </IconButton>
|
| </Tooltip>
|
| <Tooltip title={evaluation === 'failed' ? 'Remove failure rating' : 'Mark as failed'}>
|
| <IconButton
|
| size="small"
|
| onClick={() => handleTraceEvaluation('failed')}
|
| disabled={isVoting}
|
| sx={{
|
| padding: '4px',
|
| color: evaluation === 'failed' ? 'error.main' : 'action.disabled',
|
| '&:hover': {
|
| color: 'error.main',
|
| backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.08)',
|
| },
|
| }}
|
| >
|
| <ThumbDownIcon sx={{ fontSize: 18 }} />
|
| </IconButton>
|
| </Tooltip>
|
| </Box>
|
| </Box>
|
| </Box>
|
|
|
| {/* Divider before metrics */}
|
| <Divider sx={{ my: 2 }} />
|
|
|
| {/* Metrics */}
|
| <Box
|
| sx={{
|
| display: 'flex',
|
| alignItems: 'center',
|
| gap: 1.5,
|
| flexWrap: 'wrap',
|
| justifyContent: 'center',
|
| }}
|
| >
|
| {/* Agent */}
|
| {trace?.modelId && (
|
| <>
|
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
| <SmartToyIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| color: 'text.primary',
|
| fontFamily: 'monospace',
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| }}
|
| >
|
| {formatModelName(trace.modelId)}
|
| </Typography>
|
| </Box>
|
|
|
| {/* Divider */}
|
| <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
|
| </>
|
| )}
|
|
|
| {/* Steps Count */}
|
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
| <FormatListNumberedIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| color: 'text.primary',
|
| mr: 0.5,
|
| }}
|
| >
|
| {finalStep.metadata.numberOfSteps}
|
| </Typography>
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.7rem',
|
| fontWeight: 400,
|
| color: 'text.secondary',
|
| }}
|
| >
|
| {finalStep.metadata.numberOfSteps === 1 ? 'Step' : 'Steps'}
|
| </Typography>
|
| </Box>
|
|
|
| {/* Divider */}
|
| <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
|
|
|
| {/* Duration */}
|
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
| <AccessTimeIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| color: 'text.primary',
|
| }}
|
| >
|
| {finalStep.metadata.duration.toFixed(1)}s
|
| </Typography>
|
| </Box>
|
|
|
| {/* Divider */}
|
| <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
|
|
|
| {/* Input Tokens */}
|
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
| <InputIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| color: 'text.primary',
|
| }}
|
| >
|
| {finalStep.metadata.inputTokensUsed.toLocaleString()}
|
| </Typography>
|
| </Box>
|
|
|
| {/* Divider */}
|
| <Box sx={{ width: '1px', height: 16, backgroundColor: 'divider' }} />
|
|
|
| {/* Output Tokens */}
|
| <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
| <OutputIcon sx={{ fontSize: '0.85rem', color: 'primary.main' }} />
|
| <Typography
|
| variant="caption"
|
| sx={{
|
| fontSize: '0.75rem',
|
| fontWeight: 700,
|
| color: 'text.primary',
|
| }}
|
| >
|
| {finalStep.metadata.outputTokensUsed.toLocaleString()}
|
| </Typography>
|
| </Box>
|
| </Box>
|
| </Paper>
|
|
|
| {}
|
| {gifError && (
|
| <Alert severity="error" sx={{ fontSize: '0.72rem', py: 0.5 }}>
|
| {gifError}
|
| </Alert>
|
| )}
|
|
|
| {}
|
| <Box
|
| sx={{
|
| display: 'flex',
|
| flexDirection: 'column',
|
| gap: 1.5,
|
| alignItems: 'center',
|
| }}
|
| >
|
| {}
|
| <Box
|
| sx={{
|
| display: 'flex',
|
| gap: 1,
|
| justifyContent: 'center',
|
| flexWrap: 'wrap',
|
| }}
|
| >
|
| <DownloadGifButton
|
| isGenerating={isGenerating}
|
| onClick={onGenerateGif}
|
| disabled={!steps || steps.length === 0}
|
| />
|
| <DownloadJsonButton onClick={onDownloadJson} disabled={!trace} />
|
| </Box>
|
|
|
| {}
|
| <Button
|
| variant="contained"
|
| startIcon={<AddIcon sx={{ fontSize: 20 }} />}
|
| onClick={onBackToHome}
|
| color="primary"
|
| sx={{
|
| textTransform: 'none',
|
| fontWeight: 700,
|
| fontSize: '0.9rem',
|
| px: 3,
|
| py: 1,
|
| boxShadow: 2,
|
| minWidth: 200,
|
| '&:hover': {
|
| boxShadow: 4,
|
| },
|
| }}
|
| >
|
| New Task
|
| </Button>
|
| </Box>
|
| </Box>
|
| );
|
| };
|
|
|