'use client'; import React from 'react'; import { Bot, Loader2, Paperclip, User } from 'lucide-react'; import type { ChatMessage, ProcessArtifact, ProcessEvent, ProcessRun } from '@/types'; import { getAccessToken, getFileUrl } from '@/lib/api'; import { AgentTeamBlock } from '@/components/chat-workbench/AgentTeamBlock'; import { MarkdownContent } from '@/components/chat-workbench/MarkdownContent'; import { ScrollArea } from '@/components/ui/scroll-area'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; function AuthImage({ src, alt, className }: { src: string; alt: string; className?: string }) { const [blobUrl, setBlobUrl] = React.useState(null); React.useEffect(() => { const token = getAccessToken(); const headers: Record = {}; if (token) headers.Authorization = `Bearer ${token}`; let revoke: string | null = null; fetch(src, { headers }) .then((res) => res.blob()) .then((blob) => { revoke = URL.createObjectURL(blob); setBlobUrl(revoke); }) .catch(() => {}); return () => { if (revoke) URL.revokeObjectURL(revoke); }; }, [src]); if (!blobUrl) return
; return {alt}; } function MessageBubble({ message }: { message: ChatMessage }) { const isUser = message.role === 'user'; const textContent = typeof message.content === 'string' ? message.content : String(message.content || ''); return (
{!isUser && (
)}
{message.attachments && message.attachments.length > 0 && (
{message.attachments.map((att) => { const fileUrl = getFileUrl(att.file_id); if (att.content_type.startsWith('image/')) { return ( ); } return ( {att.name} {att.size && ( {att.size > 1024 * 1024 ? `${(att.size / 1024 / 1024).toFixed(1)}MB` : `${(att.size / 1024).toFixed(0)}KB`} )} ); })}
)} {isUser ? (

{textContent}

) : ( )}
{isUser && (
)}
); } type AgentTeamGroup = { rootRun: ProcessRun; memberRuns: ProcessRun[]; startedAt: string; }; const TERMINAL_RUN_STATUSES = new Set(['done', 'error', 'cancelled']); function shouldHideSystemAgentMessage(message: ChatMessage): boolean { if (message.role !== 'assistant' || typeof message.content !== 'string') { return false; } const content = message.content.trim(); return ( /^\[(Agent team|Subagent)\s+['"][^'"]+['"]\s+(completed|failed|cancelled|finished)\]/i.test(content) || (content.startsWith('[Agent team ') && content.includes('\nTask:')) ); } function parseTimelineTime(value?: string | null): number | null { if (!value) return null; const parsed = new Date(value).getTime(); return Number.isFinite(parsed) ? parsed : null; } function buildAgentTeamGroups(processRuns: ProcessRun[]): AgentTeamGroup[] { const runMap = new Map(processRuns.map((run) => [run.run_id, run])); const groups = new Map(); for (const run of processRuns) { if (run.actor_type !== 'agent') { continue; } let root = run; const seen = new Set([run.run_id]); let parentId = run.parent_run_id ?? null; while (parentId) { const parent = runMap.get(parentId); if (!parent || seen.has(parent.run_id)) { break; } root = parent; seen.add(parent.run_id); parentId = parent.parent_run_id ?? null; } const existing = groups.get(root.run_id); if (existing) { existing.memberRuns.push(run); continue; } groups.set(root.run_id, { rootRun: root, memberRuns: [run], startedAt: root.started_at || run.started_at, }); } return Array.from(groups.values()) .map((group) => ({ ...group, memberRuns: [...group.memberRuns].sort((a: ProcessRun, b: ProcessRun) => { const at = parseTimelineTime(a.started_at) ?? 0; const bt = parseTimelineTime(b.started_at) ?? 0; return at - bt; }), })) .sort((a, b) => { const at = parseTimelineTime(a.startedAt) ?? 0; const bt = parseTimelineTime(b.startedAt) ?? 0; return at - bt; }); } export function MessageList({ messages, isThinking, messagesEndRef, viewportRef, processRuns, processEvents, processArtifacts, selectedRunId, onSelectRun, onCancelRun, }: { messages: ChatMessage[]; isThinking: boolean; messagesEndRef: React.RefObject; viewportRef: React.RefObject; processRuns: ProcessRun[]; processEvents: ProcessEvent[]; processArtifacts: ProcessArtifact[]; selectedRunId: string | null; onSelectRun: (runId: string) => void; onCancelRun: (runId: string) => void; }) { const { locale } = useAppI18n(); const visibleMessages = React.useMemo( () => messages.filter((message) => !shouldHideSystemAgentMessage(message)), [messages] ); const teamGroups = React.useMemo( () => buildAgentTeamGroups(processRuns).filter((group) => group.memberRuns.some((run) => !TERMINAL_RUN_STATUSES.has(run.status)) ), [processRuns] ); const timelineItems = React.useMemo(() => { const messageItems = visibleMessages.map((message, index) => ({ kind: 'message' as const, key: `${message.role}:${message.timestamp || index}:${index}`, sortTime: parseTimelineTime(message.timestamp) ?? Number.MAX_SAFE_INTEGER / 2 + index, order: index, message, })); const teamItems = teamGroups.map((group, index) => ({ kind: 'team' as const, key: `team:${group.rootRun.run_id}`, sortTime: parseTimelineTime(group.startedAt) ?? Number.MAX_SAFE_INTEGER / 2 + visibleMessages.length + index, order: visibleMessages.length + index, group, })); return [...messageItems, ...teamItems].sort((a, b) => { if (a.sortTime !== b.sortTime) { return a.sortTime - b.sortTime; } return a.order - b.order; }); }, [teamGroups, visibleMessages]); return (
{visibleMessages.length === 0 && teamGroups.length === 0 && !isThinking && (

Boardware Agent Sandbox

{pickAppText(locale, '发送消息开始对话', 'Send a message to start the conversation')}

)} {timelineItems.map((item) => item.kind === 'message' ? ( ) : ( ) )} {isThinking && (
{pickAppText(locale, '思考中...', 'Thinking...')}
)}
); }