From 18381ec2830300b2b10883203d2eb80e58b9b566 Mon Sep 17 00:00:00 2001 From: kvtodev Date: Tue, 14 Jul 2026 10:51:31 +0800 Subject: [PATCH] impl simple display --- .../EditorAgentConversation/MessageBubble.tsx | 106 ++++++++++-------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/src/components/image-editor/EditorAgentConversation/MessageBubble.tsx b/src/components/image-editor/EditorAgentConversation/MessageBubble.tsx index 94545f036..c59a6ab70 100644 --- a/src/components/image-editor/EditorAgentConversation/MessageBubble.tsx +++ b/src/components/image-editor/EditorAgentConversation/MessageBubble.tsx @@ -1,20 +1,17 @@ import { Check, Image as ImageIcon, Loader2, Volume2, X } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; - import type { EditorAgentAttachmentRef, EditorAgentMessage, EditorAgentToolCall, } from '@/packages/shared/src/contracts'; -import type { ExternalGenerationJobStatus } from '@/packages/shared/src/contracts/externalGeneration.ts'; -import type { ExternalGenerationJobStatusRecord } from '@/packages/shared/src/contracts/externalGeneration.ts'; +import { getExternalGenerationJobStatus } from '@/src/services/external-generation'; import { attachmentKey } from '@/src/components/image-editor/EditorAgentConversation/common.ts'; import { PendingToolCall } from '@/src/components/image-editor/EditorAgentConversation/PendingToolCall.tsx'; 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'; function messageRoleLabel(role: EditorAgentMessage['role']) { if (role === 'user') { @@ -62,76 +59,92 @@ export function AttachmentChip({ function ToolCallView({ toolCall, - jobStatus, onJobCompleted, }: { toolCall: EditorAgentToolCall; - jobStatus?: ExternalGenerationJobStatus | null; onJobCompleted?: () => void; }) { const videos = toolCall.videos ?? []; const audios = toolCall.audios ?? []; - const [resolvedJob, setResolvedJob] = - useState(null); + const jobId = toolCall.externalJobId?.trim() || null; + const displaySourceKey = jobId ?? toolCall.status; + const initialDisplayStatus = + toolCall.status === 'completed' + ? 'completed' + : toolCall.status === 'failed' + ? 'failed' + : toolCall.status === 'cancelled' + ? 'cancelled' + : 'pending'; + const [displayStatus, setDisplayStatus] = useState(initialDisplayStatus); + const [displayError, setDisplayError] = useState( + toolCall.error ?? null, + ); const completionNotifiedRef = useRef(false); + const onJobCompletedRef = useRef(onJobCompleted); + useEffect(() => { + onJobCompletedRef.current = onJobCompleted; + }, [onJobCompleted]); useEffect(() => { - const jobId = toolCall.externalJobId?.trim(); if (!jobId) { - setResolvedJob(null); + completionNotifiedRef.current = false; return; } + // Only a different job may reinitialize the local display state. + setDisplayStatus(initialDisplayStatus); + setDisplayError(toolCall.error ?? null); + completionNotifiedRef.current = false; let disposed = false; let timeoutId: ReturnType | undefined; - const refresh = async () => { + const poll = async () => { try { const response = await getExternalGenerationJobStatus(jobId); if (disposed) return; - setResolvedJob(response.job); if ( - response.job.status === 'completed' && - !completionNotifiedRef.current + response.job.status === 'completed' || + response.job.status === 'failed' ) { - completionNotifiedRef.current = true; - onJobCompleted?.(); - } - if ( - response.job.status === 'queued' || - response.job.status === 'running' - ) { - timeoutId = setTimeout(refresh, 1500); + setDisplayStatus(response.job.status); + setDisplayError(response.job.error ?? null); + if ( + response.job.status === 'completed' && + !completionNotifiedRef.current + ) { + completionNotifiedRef.current = true; + onJobCompletedRef.current?.(); + } + return; } + timeoutId = setTimeout(poll, 1500); } catch { - if (!disposed) timeoutId = setTimeout(refresh, 3000); + if (!disposed) timeoutId = setTimeout(poll, 3000); } }; - void refresh(); + void poll(); return () => { disposed = true; if (timeoutId) clearTimeout(timeoutId); }; - }, [onJobCompleted, toolCall.externalJobId]); - const effectiveJobStatus = jobStatus ?? resolvedJob?.status ?? null; - const isCancelled = Boolean(toolCall.cancelledAt); - const isExecuting = - effectiveJobStatus === 'queued' || effectiveJobStatus === 'running'; - const statusLabel = - effectiveJobStatus === 'completed' - ? '已完成' + }, [displaySourceKey]); + const isCancelled = displayStatus === 'cancelled'; + const isCompleted = displayStatus === 'completed'; + const isFailed = displayStatus === 'failed'; + const isExecuting = Boolean(jobId) && displayStatus === 'pending'; + const statusLabel = isCompleted + ? '已完成' + : isFailed + ? '失败' : isCancelled ? '已取消' - : effectiveJobStatus === 'failed' - ? '失败' - : !toolCall.externalJobId - ? '待确认' - : '执行中'; + : '待确认'; return (
{isExecuting ? (
- {toolCall.error || resolvedJob?.error ? ( -
- {toolCall.error ?? resolvedJob?.error} -
+ {displayError ? ( +
{displayError}
) : null} {toolCall.images.length ? (
@@ -241,7 +252,6 @@ export function ThinkingBubble() { type MessageBubbleProps = { message: EditorAgentMessage; - jobStatus?: ExternalGenerationJobStatus | null; busyAction: 'confirm' | 'cancel' | null; onConfirmToolCall: (messageId: number) => void; onCancelToolCall: (messageId: number) => void; @@ -250,7 +260,6 @@ type MessageBubbleProps = { export function MessageBubble({ message, - jobStatus, busyAction, onConfirmToolCall, onCancelToolCall, @@ -262,8 +271,8 @@ export function MessageBubble({ if ( message.role === 'system' && message.toolCall && - !message.toolCall.externalJobId && - !message.toolCall.cancelledAt + message.toolCall.status === 'not_completed' && + !message.toolCall.externalJobId ) { return ( ) : null}