import { Check, Image as ImageIcon, Loader2, Volume2, X } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import type { EditorAgentToolCall } from '@/packages/shared/src/contracts'; import { editorAgentToolLabel } from '@/src/components/image-editor/EditorAgentConversation/toolCallPresentation.ts'; import { ResolvedAssetAudio } from '@/src/components/ResolvedAssetAudio.tsx'; import { ResolvedAssetImage } from '@/src/components/ResolvedAssetImage.tsx'; import { ResolvedAssetVideo } from '@/src/components/ResolvedAssetVideo.tsx'; import { getExternalGenerationJobStatus } from '@/src/services/external-generation'; import type { RightClickMenuHandler } from './common.ts'; function ToolCallView({ toolCall, onJobCompleted, onRightClickMenu, }: { toolCall: EditorAgentToolCall; onJobCompleted?: () => void; onRightClickMenu?: RightClickMenuHandler; }) { const videos = toolCall.videos ?? []; const audios = toolCall.audios ?? []; const jobId = toolCall.externalJobId?.trim() || null; const initialDisplayStatus = toolCall.status === 'completed' ? 'completed' : toolCall.status === 'failed' ? 'failed' : toolCall.status === 'cancelled' ? 'cancelled' : 'pending'; const initialDisplayError = toolCall.error ?? null; const shouldPoll = toolCall.status === 'not_completed' && Boolean(jobId); const [displayStatus, setDisplayStatus] = useState(initialDisplayStatus); const [displayError, setDisplayError] = useState(initialDisplayError); const terminalNotifiedRef = useRef(false); const onJobCompletedRef = useRef(onJobCompleted); useEffect(() => { onJobCompletedRef.current = onJobCompleted; }, [onJobCompleted]); useEffect(() => { setDisplayStatus(initialDisplayStatus); setDisplayError(initialDisplayError); terminalNotifiedRef.current = false; if (!shouldPoll || !jobId) return; let disposed = false; let timeoutId: ReturnType | undefined; const poll = async () => { try { const response = await getExternalGenerationJobStatus(jobId); if (disposed) return; if ( response.job.status === 'completed' || response.job.status === 'failed' ) { setDisplayStatus(response.job.status); setDisplayError(response.job.error ?? null); if (!terminalNotifiedRef.current) { terminalNotifiedRef.current = true; onJobCompletedRef.current?.(); } return; } timeoutId = setTimeout(poll, 1500); } catch { if (!disposed) timeoutId = setTimeout(poll, 3000); } }; void poll(); return () => { disposed = true; if (timeoutId) clearTimeout(timeoutId); }; // 轮询只允许新的 job/status source 重置;其余值通过当前 source 对应的闭包读取。 // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialDisplayError, initialDisplayStatus, jobId, shouldPoll]); const isCancelled = displayStatus === 'cancelled'; const isCompleted = displayStatus === 'completed'; const isFailed = displayStatus === 'failed'; const isExecuting = displayStatus === 'pending' && Boolean(jobId); const statusLabel = isCompleted ? '已完成' : isFailed ? '失败' : isCancelled ? '已取消' : isExecuting ? '执行中' : '待确认'; return (
{isExecuting ? (
{displayError ? (
{displayError}
) : null} {toolCall.images.length ? (
{toolCall.images.map((image, index) => (
onRightClickMenu(event, { kind: 'generated_media', mediaType: 'image', mediaSrc: image.imageSrc, objectKey: image.objectKey, suggestedFileName: `Agent生成图片-${index + 1}`, }) : undefined } >
))}
) : null} {videos.length ? (
{videos.map((video, index) => (
onRightClickMenu(event, { kind: 'generated_media', mediaType: 'video', mediaSrc: video.videoSrc, objectKey: video.objectKey, suggestedFileName: `Agent生成视频-${index + 1}`, }) : undefined } >
))}
) : null} {audios.length ? (
{audios.map((audio, index) => (
onRightClickMenu(event, { kind: 'generated_media', mediaType: 'audio', mediaSrc: audio.audioSrc, objectKey: audio.objectKey, suggestedFileName: `Agent生成音频-${index + 1}`, }) : undefined } >
))}
) : null}
); } export default ToolCallView;