import { updateStepEvaluation } from '@/services/api'; import { useAgentStore } from '@/stores/agentStore'; import { AgentStep } from '@/types/agent'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import InputIcon from '@mui/icons-material/Input'; import OutputIcon from '@mui/icons-material/Output'; import ThumbDownIcon from '@mui/icons-material/ThumbDown'; import ThumbUpIcon from '@mui/icons-material/ThumbUp'; import { Accordion, AccordionDetails, AccordionSummary, Box, Card, CardContent, Chip, IconButton, Tooltip, Typography } from '@mui/material'; import React, { useState } from 'react'; interface StepCardProps { step: AgentStep; index: number; isLatest?: boolean; isActive?: boolean; } export const StepCard: React.FC = ({ step, index, isLatest = false, isActive = false }) => { const setSelectedStepIndex = useAgentStore((state) => state.setSelectedStepIndex); const updateStepEvaluationInStore = useAgentStore((state) => state.updateStepEvaluation); const [thoughtExpanded, setThoughtExpanded] = useState(false); const [actionsExpanded, setActionsExpanded] = useState(false); const [evaluation, setEvaluation] = useState<'like' | 'dislike' | 'neutral'>(step.step_evaluation || 'neutral'); const [isVoting, setIsVoting] = useState(false); const hasMultipleActions = step.actions && step.actions.length > 1; const displayedActions = hasMultipleActions && !actionsExpanded ? step.actions.slice(0, 1) : step.actions; const handleClick = () => { setSelectedStepIndex(index); }; const handleAccordionClick = (event: React.MouseEvent) => { event.stopPropagation(); // Prevent propagation to avoid selecting the step }; const handleVote = async (event: React.MouseEvent, vote: 'like' | 'dislike') => { event.stopPropagation(); // Prevent propagation to avoid selecting the step if (isVoting) return; const newEvaluation = evaluation === vote ? 'neutral' : vote; setIsVoting(true); try { await updateStepEvaluation(step.traceId, step.stepId, newEvaluation); setEvaluation(newEvaluation); // Update the store so the evaluation is reflected in JSON export updateStepEvaluationInStore(step.stepId, newEvaluation); } catch (error) { console.error('Failed to update step evaluation:', error); } finally { setIsVoting(false); } }; return ( `${isActive ? theme.palette.primary.main : theme.palette.divider} !important`, borderRadius: 1.5, transition: 'all 0.2s ease', cursor: 'pointer', boxShadow: isActive ? (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.3)' : 'rgba(79, 134, 198, 0.2)'}` : 'none', '&:hover': { borderColor: (theme) => `${theme.palette.primary.main} !important`, boxShadow: (theme) => `0 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(79, 134, 198, 0.2)' : 'rgba(79, 134, 198, 0.1)'}`, }, }} > {/* Step header */} {index + 1} } label={`${step.duration.toFixed(1)}s`} size="small" sx={{ height: 'auto', py: 0.25, fontSize: '0.65rem', fontWeight: 600, backgroundColor: 'action.hover', color: 'text.primary', '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' }, }} /> } label={step.inputTokensUsed.toLocaleString()} size="small" sx={{ height: 'auto', py: 0.25, fontSize: '0.65rem', fontWeight: 600, backgroundColor: 'action.hover', color: 'text.primary', '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' }, }} /> } label={step.outputTokensUsed.toLocaleString()} size="small" sx={{ height: 'auto', py: 0.25, fontSize: '0.65rem', fontWeight: 600, backgroundColor: 'action.hover', color: 'text.primary', '& .MuiChip-icon': { marginLeft: 0.5, color: 'text.secondary' }, }} /> {/* Step image */} {step.image && ( isActive ? theme.palette.primary.main : theme.palette.divider, backgroundColor: 'action.hover', transition: 'border-color 0.2s ease', }} > {`Step )} {/* Action */} {step.actions && step.actions.length > 0 && ( Action {hasMultipleActions && ( { e.stopPropagation(); setActionsExpanded(!actionsExpanded); }} sx={{ padding: '2px', color: 'text.secondary', '&:hover': { color: 'text.primary', backgroundColor: 'action.hover', }, }} > )} {/* Vote buttons */} handleVote(e, 'like')} disabled={isVoting} sx={{ padding: '2px', color: evaluation === 'like' ? '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)', }, }} > handleVote(e, 'dislike')} disabled={isVoting} sx={{ padding: '2px', color: evaluation === 'dislike' ? '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)', }, }} > {displayedActions?.map((action, actionIndex) => ( {/* */} {action.description} ))} )} {/* Thought - Accordion */} {step.thought && ( setThoughtExpanded(expanded)} onClick={handleAccordionClick} elevation={0} disableGutters sx={{ mb: 0.5, backgroundColor: 'transparent', border: 'none', boxShadow: 'none', '&:before': { display: 'none' }, '&.MuiAccordion-root': { backgroundColor: 'transparent', boxShadow: 'none', '&:before': { display: 'none', }, }, '& .MuiAccordionSummary-root': { minHeight: 'auto', p: 0, backgroundColor: 'transparent', '&:hover': { backgroundColor: 'transparent', }, '&.Mui-expanded': { minHeight: 'auto', }, }, '& .MuiAccordionSummary-content': { margin: '0 !important', }, '& .MuiAccordionDetails-root': { p: 0, pt: 0.5, pb: 0, backgroundColor: 'transparent', }, }} > } sx={{ flexDirection: 'row', border: 'none', '& .MuiAccordionSummary-expandIconWrapper': { transform: 'rotate(-90deg)', transition: 'transform 0.2s', '&.Mui-expanded': { transform: 'rotate(0deg)', }, }, }} > Thought {step.thought} )} {/* Error */} {step.error && ( theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.08)', border: '1px solid', borderColor: 'error.main' }}> Error: {step.error} )} ); };