impl simple display
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user