ml-agent / frontend /src /components /Chat /AssistantMessage.tsx
akseljoonas's picture
akseljoonas HF Staff
feat: merge HF Space improvements
bdbcdab
raw
history blame
3.66 kB
import { useMemo } from 'react';
import { Box, Stack, Typography } from '@mui/material';
import MarkdownContent from './MarkdownContent';
import ToolCallGroup from './ToolCallGroup';
import type { UIMessage } from 'ai';
import type { MessageMeta } from '@/types/agent';
interface AssistantMessageProps {
message: UIMessage;
isStreaming?: boolean;
approveTools: (approvals: Array<{ tool_call_id: string; approved: boolean; feedback?: string | null }>) => Promise<boolean>;
}
/**
* Groups consecutive tool parts together so they render as a single
* ToolCallGroup (visually identical to the old segments approach).
*/
type DynamicToolPart = Extract<UIMessage['parts'][number], { type: 'dynamic-tool' }>;
function groupParts(parts: UIMessage['parts']) {
const groups: Array<
| { kind: 'text'; text: string; idx: number }
| { kind: 'tools'; tools: DynamicToolPart[]; idx: number }
> = [];
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.type === 'text') {
groups.push({ kind: 'text', text: part.text, idx: i });
} else if (part.type === 'dynamic-tool') {
const toolPart = part as DynamicToolPart;
const last = groups[groups.length - 1];
if (last?.kind === 'tools') {
last.tools.push(toolPart);
} else {
groups.push({ kind: 'tools', tools: [toolPart], idx: i });
}
}
// step-start, step-end, etc. are ignored visually
}
return groups;
}
export default function AssistantMessage({ message, isStreaming = false, approveTools }: AssistantMessageProps) {
const groups = useMemo(() => groupParts(message.parts), [message.parts]);
// Find the last text group index for streaming cursor
let lastTextIdx = -1;
for (let i = groups.length - 1; i >= 0; i--) {
if (groups[i].kind === 'text') { lastTextIdx = i; break; }
}
const meta = message.metadata as MessageMeta | undefined;
const timeStr = meta?.createdAt
? new Date(meta.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
: null;
if (groups.length === 0) return null;
return (
<Box sx={{ minWidth: 0 }}>
<Stack direction="row" alignItems="baseline" spacing={1} sx={{ mb: 0.5 }}>
<Typography
variant="caption"
sx={{
fontWeight: 700,
fontSize: '0.72rem',
color: 'var(--muted-text)',
textTransform: 'uppercase',
letterSpacing: '0.04em',
}}
>
Assistant
</Typography>
{timeStr && (
<Typography variant="caption" sx={{ color: 'var(--muted-text)', fontSize: '0.7rem' }}>
{timeStr}
</Typography>
)}
</Stack>
<Box
sx={{
maxWidth: { xs: '95%', md: '85%' },
bgcolor: 'var(--surface)',
borderRadius: 1.5,
borderTopLeftRadius: 4,
px: { xs: 1.5, md: 2.5 },
py: 1.5,
border: '1px solid var(--border)',
}}
>
{groups.map((group, i) => {
if (group.kind === 'text' && group.text) {
return (
<MarkdownContent
key={group.idx}
content={group.text}
isStreaming={isStreaming && i === lastTextIdx}
/>
);
}
if (group.kind === 'tools' && group.tools.length > 0) {
return (
<ToolCallGroup
key={group.idx}
tools={group.tools}
approveTools={approveTools}
/>
);
}
return null;
})}
</Box>
</Box>
);
}