impl simple display

This commit is contained in:
2026-07-14 10:51:31 +08:00
parent 21c39ee8db
commit 18381ec283
@@ -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<ExternalGenerationJobStatusRecord | null>(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<string | null>(
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<typeof setTimeout> | 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 (
<div className="rounded-lg border border-slate-200 bg-white/80 p-2 text-xs text-slate-600">
<div className="flex items-center gap-2">
{isExecuting ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" aria-hidden="true" />
) : effectiveJobStatus === 'completed' ? (
) : isCompleted ? (
<Check className="h-3.5 w-3.5" aria-hidden="true" />
) : isCancelled ? (
) : isCancelled || isFailed ? (
<X className="h-3.5 w-3.5" aria-hidden="true" />
) : (
<ImageIcon className="h-3.5 w-3.5" aria-hidden="true" />
@@ -139,10 +152,8 @@ function ToolCallView({
<span>{editorAgentToolLabel(toolCall.toolName)}</span>
<span className="ml-auto text-slate-400">{statusLabel}</span>
</div>
{toolCall.error || resolvedJob?.error ? (
<div className="mt-1 text-red-600">
{toolCall.error ?? resolvedJob?.error}
</div>
{displayError ? (
<div className="mt-1 text-red-600">{displayError}</div>
) : null}
{toolCall.images.length ? (
<div className="mt-2 grid grid-cols-3 gap-2">
@@ -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 (
<PendingToolCall
@@ -292,7 +301,7 @@ export function MessageBubble({
: `max-w-[86%] rounded-3xl px-3.5 py-3 text-sm leading-6 shadow-sm ${
isUser
? 'bg-slate-900 text-white'
: jobStatus === 'failed'
: message.toolCall?.status === 'failed'
? 'border border-red-200 bg-red-50 text-red-700'
: 'border border-slate-200 bg-white text-slate-700'
}`
@@ -314,7 +323,6 @@ export function MessageBubble({
{message.toolCall ? (
<ToolCallView
toolCall={message.toolCall}
jobStatus={jobStatus}
onJobCompleted={onJobCompleted}
/>
) : null}