|
|
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<StepCardProps> = ({ 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(); |
|
|
}; |
|
|
|
|
|
const handleVote = async (event: React.MouseEvent, vote: 'like' | 'dislike') => { |
|
|
event.stopPropagation(); |
|
|
|
|
|
if (isVoting) return; |
|
|
|
|
|
const newEvaluation = evaluation === vote ? 'neutral' : vote; |
|
|
setIsVoting(true); |
|
|
|
|
|
try { |
|
|
await updateStepEvaluation(step.traceId, step.stepId, newEvaluation); |
|
|
setEvaluation(newEvaluation); |
|
|
|
|
|
updateStepEvaluationInStore(step.stepId, newEvaluation); |
|
|
} catch (error) { |
|
|
console.error('Failed to update step evaluation:', error); |
|
|
} finally { |
|
|
setIsVoting(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
return ( |
|
|
<Card |
|
|
elevation={0} |
|
|
onClick={handleClick} |
|
|
sx={{ |
|
|
backgroundColor: 'background.paper', |
|
|
border: '1px solid', |
|
|
borderColor: (theme) => `${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)'}`, |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<CardContent sx={{ p: 1.5, '&:last-child': { pb: 1.5 } }}> |
|
|
{/* Step header */} |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}> |
|
|
<Typography |
|
|
sx={{ |
|
|
fontSize: '1.5rem', |
|
|
fontWeight: 800, |
|
|
color: isActive ? 'primary.main' : 'text.primary', |
|
|
lineHeight: 1, |
|
|
}} |
|
|
> |
|
|
{index + 1} |
|
|
</Typography> |
|
|
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}> |
|
|
<Chip |
|
|
icon={<AccessTimeIcon sx={{ fontSize: '0.7rem !important' }} />} |
|
|
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' }, |
|
|
}} |
|
|
/> |
|
|
<Chip |
|
|
icon={<InputIcon sx={{ fontSize: '0.7rem !important' }} />} |
|
|
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' }, |
|
|
}} |
|
|
/> |
|
|
<Chip |
|
|
icon={<OutputIcon sx={{ fontSize: '0.7rem !important' }} />} |
|
|
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' }, |
|
|
}} |
|
|
/> |
|
|
</Box> |
|
|
</Box> |
|
|
|
|
|
{/* Step image */} |
|
|
{step.image && ( |
|
|
<Box |
|
|
sx={{ |
|
|
mb: 1.5, |
|
|
borderRadius: 1, |
|
|
overflow: 'hidden', |
|
|
border: '1px solid', |
|
|
borderColor: (theme) => isActive ? theme.palette.primary.main : theme.palette.divider, |
|
|
backgroundColor: 'action.hover', |
|
|
transition: 'border-color 0.2s ease', |
|
|
}} |
|
|
> |
|
|
<img |
|
|
src={step.image} |
|
|
alt={`Step ${index + 1}`} |
|
|
style={{ width: '100%', height: 'auto', display: 'block' }} |
|
|
/> |
|
|
</Box> |
|
|
)} |
|
|
|
|
|
{/* Action */} |
|
|
{step.actions && step.actions.length > 0 && ( |
|
|
<Box sx={{ mb: 1.5 }}> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mb: 0.75, justifyContent: 'space-between' }}> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontWeight: 700, |
|
|
color: 'text.secondary', |
|
|
fontSize: '0.65rem', |
|
|
textTransform: 'uppercase', |
|
|
letterSpacing: '0.5px', |
|
|
}} |
|
|
> |
|
|
Action |
|
|
</Typography> |
|
|
{hasMultipleActions && ( |
|
|
<Tooltip title={actionsExpanded ? 'Show less' : `Show all ${step.actions.length} actions`}> |
|
|
<IconButton |
|
|
size="small" |
|
|
onClick={(e) => { |
|
|
e.stopPropagation(); |
|
|
setActionsExpanded(!actionsExpanded); |
|
|
}} |
|
|
sx={{ |
|
|
padding: '2px', |
|
|
color: 'text.secondary', |
|
|
'&:hover': { |
|
|
color: 'text.primary', |
|
|
backgroundColor: 'action.hover', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<ExpandMoreIcon |
|
|
sx={{ |
|
|
fontSize: 16, |
|
|
transform: actionsExpanded ? 'rotate(180deg)' : 'rotate(0deg)', |
|
|
transition: 'transform 0.2s', |
|
|
}} |
|
|
/> |
|
|
</IconButton> |
|
|
</Tooltip> |
|
|
)} |
|
|
</Box> |
|
|
|
|
|
{/* Vote buttons */} |
|
|
<Box sx={{ display: 'flex', gap: 0.5 }}> |
|
|
<Tooltip title={evaluation === 'like' ? 'Remove like' : 'Like this step'}> |
|
|
<IconButton |
|
|
size="small" |
|
|
onClick={(e) => 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)', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<ThumbUpIcon sx={{ fontSize: 14 }} /> |
|
|
</IconButton> |
|
|
</Tooltip> |
|
|
<Tooltip title={evaluation === 'dislike' ? 'Remove dislike' : 'Dislike this step'}> |
|
|
<IconButton |
|
|
size="small" |
|
|
onClick={(e) => 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)', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<ThumbDownIcon sx={{ fontSize: 14 }} /> |
|
|
</IconButton> |
|
|
</Tooltip> |
|
|
</Box> |
|
|
</Box> |
|
|
<Box component="ul" sx={{ listStyle: 'none', p: 0, m: 0 }}> |
|
|
{displayedActions?.map((action, actionIndex) => ( |
|
|
<Box |
|
|
key={actionIndex} |
|
|
component="li" |
|
|
sx={{ |
|
|
display: 'flex', |
|
|
alignItems: 'flex-start', |
|
|
fontSize: '0.75rem', |
|
|
color: 'text.primary', |
|
|
lineHeight: 1.4, |
|
|
mb: 0.5, |
|
|
'&:last-child': { mb: 0 }, |
|
|
}} |
|
|
> |
|
|
{/* <Typography |
|
|
component="span" |
|
|
sx={{ |
|
|
mr: 0.5, |
|
|
color: 'text.secondary', |
|
|
fontWeight: 700, |
|
|
flexShrink: 0, |
|
|
fontSize: '0.75rem', |
|
|
}} |
|
|
> |
|
|
→ |
|
|
</Typography> */} |
|
|
<Typography |
|
|
component="span" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
fontWeight: 900, |
|
|
wordBreak: 'break-word', |
|
|
}} |
|
|
> |
|
|
{action.description} |
|
|
</Typography> |
|
|
</Box> |
|
|
))} |
|
|
</Box> |
|
|
</Box> |
|
|
)} |
|
|
|
|
|
{/* Thought - Accordion */} |
|
|
{step.thought && ( |
|
|
<Accordion |
|
|
expanded={thoughtExpanded} |
|
|
onChange={(e, expanded) => 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', |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<AccordionSummary |
|
|
expandIcon={<ExpandMoreIcon sx={{ fontSize: 16, color: 'text.secondary' }} />} |
|
|
sx={{ |
|
|
flexDirection: 'row', |
|
|
border: 'none', |
|
|
'& .MuiAccordionSummary-expandIconWrapper': { |
|
|
transform: 'rotate(-90deg)', |
|
|
transition: 'transform 0.2s', |
|
|
'&.Mui-expanded': { |
|
|
transform: 'rotate(0deg)', |
|
|
}, |
|
|
}, |
|
|
}} |
|
|
> |
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontWeight: 700, |
|
|
color: 'text.secondary', |
|
|
fontSize: '0.65rem', |
|
|
textTransform: 'uppercase', |
|
|
letterSpacing: '0.5px', |
|
|
}} |
|
|
> |
|
|
Thought |
|
|
</Typography> |
|
|
</Box> |
|
|
</AccordionSummary> |
|
|
<AccordionDetails> |
|
|
<Typography |
|
|
variant="body2" |
|
|
sx={{ |
|
|
fontSize: '0.75rem', |
|
|
color: 'text.primary', |
|
|
lineHeight: 1.4, |
|
|
pl: 2.5, |
|
|
}} |
|
|
> |
|
|
{step.thought} |
|
|
</Typography> |
|
|
</AccordionDetails> |
|
|
</Accordion> |
|
|
)} |
|
|
|
|
|
{/* Error */} |
|
|
{step.error && ( |
|
|
<Box sx={{ |
|
|
mt: 1.5, |
|
|
p: 1, |
|
|
borderRadius: 1, |
|
|
backgroundColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(244, 67, 54, 0.1)' : 'rgba(244, 67, 54, 0.08)', |
|
|
border: '1px solid', |
|
|
borderColor: 'error.main' |
|
|
}}> |
|
|
<Typography |
|
|
variant="caption" |
|
|
sx={{ |
|
|
fontSize: '0.7rem', |
|
|
color: 'error.main', |
|
|
fontWeight: 600, |
|
|
}} |
|
|
> |
|
|
Error: {step.error} |
|
|
</Typography> |
|
|
</Box> |
|
|
)} |
|
|
</CardContent> |
|
|
</Card> |
|
|
); |
|
|
}; |
|
|
|