|
|
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>
|
|
|
);
|
|
|
};
|
|
|
|