diff --git a/src/components/image-editor/ImageCanvasEditorModel.test.ts b/src/components/image-editor/ImageCanvasEditorModel.test.ts
index 935f5be14..ec8ad5ca5 100644
--- a/src/components/image-editor/ImageCanvasEditorModel.test.ts
+++ b/src/components/image-editor/ImageCanvasEditorModel.test.ts
@@ -569,6 +569,8 @@ describe('ImageCanvasEditorModel', () => {
composerOpen: false,
generatedLayerId: 'layer-generated',
imageModel: 'gpt-image-2',
+ generationStartedAt: 1_771_400_000_000,
+ generationFinishedAt: 1_771_400_004_000,
placeholder: {
x: 100,
y: 120,
@@ -624,6 +626,8 @@ describe('ImageCanvasEditorModel', () => {
status: 'generating',
generatedLayerId: 'layer-generated',
imageModel: 'gpt-image-2',
+ generationStartedAt: 1_771_400_000_000,
+ generationFinishedAt: 1_771_400_004_000,
placeholder: {
x: 100,
y: 120,
diff --git a/src/components/image-editor/ImageCanvasEditorModel.ts b/src/components/image-editor/ImageCanvasEditorModel.ts
index dc772ff27..435a3c146 100644
--- a/src/components/image-editor/ImageCanvasEditorModel.ts
+++ b/src/components/image-editor/ImageCanvasEditorModel.ts
@@ -592,6 +592,8 @@ export function hydrateCanvasGenerationDialog(
aspectRatio: stringOrUndefined(snapshot.aspectRatio),
imageSize: stringOrUndefined(snapshot.imageSize),
errorMessage: stringOrUndefined(snapshot.errorMessage),
+ generationStartedAt: numberOrUndefined(snapshot.generationStartedAt),
+ generationFinishedAt: numberOrUndefined(snapshot.generationFinishedAt),
placeholder: hydrateGenerationPlaceholder(snapshot.placeholder),
};
}
@@ -823,6 +825,12 @@ export function numberFromSnapshot(value: unknown, fallback: number) {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}
+function numberOrUndefined(value: unknown) {
+ return typeof value === 'number' && Number.isFinite(value)
+ ? value
+ : undefined;
+}
+
function audioDurationOrNull(value: unknown) {
if (value && typeof value === 'object' && 'durationSeconds' in value) {
return audioDurationOrNull(
diff --git a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx
index 3e10708ce..623d6d43a 100644
--- a/src/components/image-editor/ImageCanvasEditorShellView.test.tsx
+++ b/src/components/image-editor/ImageCanvasEditorShellView.test.tsx
@@ -133,7 +133,6 @@ function createStageProps(): ImageCanvasStageViewProps {
hoveredLayerId: null,
canvasMarquee: null,
canvasGenerationDialogs: [],
- backgroundRemovalTasks: [],
isTaskSidebarOpen: false,
generateDialog: null,
cropExpandPanel: null,
@@ -176,8 +175,6 @@ function createStageProps(): ImageCanvasStageViewProps {
onUpdateLayerAssetKind: vi.fn(),
onGenerationFramePointerDown: vi.fn(),
onActivateGenerationDialog: vi.fn(),
- onFocusGenerationDialog: vi.fn(),
- onFocusTaskLayer: vi.fn(),
onFocusExternalTask: vi.fn(),
onToggleTaskSidebar: vi.fn(),
onCropExpandHandlePointerDown: vi.fn(),
diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx
index 8b6e858a2..3d1ad0012 100644
--- a/src/components/image-editor/ImageCanvasEditorView.tsx
+++ b/src/components/image-editor/ImageCanvasEditorView.tsx
@@ -1678,7 +1678,6 @@ export function ImageCanvasEditorView() {
hoveredLayerId,
canvasMarquee,
canvasGenerationDialogs,
- backgroundRemovalTasks: generationSurface.backgroundRemovalTasks,
isTaskSidebarOpen: generationSurface.isTaskSidebarOpen,
generateDialog,
cropExpandPanel: generationSurface.cropExpandPanel,
@@ -1726,8 +1725,6 @@ export function ImageCanvasEditorView() {
onUpdateLayerAssetKind: updateLayerAssetKind,
onGenerationFramePointerDown: handleGenerationFramePointerDown,
onActivateGenerationDialog: activateCanvasGenerationDialog,
- onFocusGenerationDialog: focusCanvasGenerationDialog,
- onFocusTaskLayer: focusCanvasLayerById,
onFocusExternalTask: focusExternalGenerationTask,
onToggleTaskSidebar: generationSurface.toggleTaskSidebar,
onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize,
diff --git a/src/components/image-editor/ImageCanvasStageView.tsx b/src/components/image-editor/ImageCanvasStageView.tsx
index c731cd54c..ebe47c183 100644
--- a/src/components/image-editor/ImageCanvasStageView.tsx
+++ b/src/components/image-editor/ImageCanvasStageView.tsx
@@ -17,7 +17,6 @@ import type {
CanvasClipboard,
CanvasContextMenuState,
CanvasGenerationDialogState,
- CanvasTaskListItem,
CanvasLayer,
CanvasMarqueeState,
CanvasTool,
@@ -58,7 +57,6 @@ export type ImageCanvasStageViewProps = {
hoveredLayerId: string | null;
canvasMarquee: CanvasMarqueeState | null;
canvasGenerationDialogs: CanvasGenerationDialogState[];
- backgroundRemovalTasks: CanvasTaskListItem[];
isTaskSidebarOpen: boolean;
generateDialog: GenerateDialogState | null;
cropExpandPanel: CropExpandPanelState | null;
@@ -117,8 +115,6 @@ export type ImageCanvasStageViewProps = {
dialog: CanvasGenerationDialogState,
) => void;
onActivateGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
- onFocusGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
- onFocusTaskLayer: (layerId: string) => void;
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
onToggleTaskSidebar: () => void;
onCropExpandHandlePointerDown: (
@@ -199,7 +195,6 @@ export function ImageCanvasStageView({
hoveredLayerId,
canvasMarquee,
canvasGenerationDialogs,
- backgroundRemovalTasks,
isTaskSidebarOpen,
generateDialog,
cropExpandPanel,
@@ -243,8 +238,6 @@ export function ImageCanvasStageView({
onUpdateLayerAssetKind,
onGenerationFramePointerDown,
onActivateGenerationDialog,
- onFocusGenerationDialog,
- onFocusTaskLayer,
onFocusExternalTask,
onToggleTaskSidebar,
onCropExpandHandlePointerDown,
@@ -445,12 +438,8 @@ export function ImageCanvasStageView({
diff --git a/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx b/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx
index d8640f038..c56213bcb 100644
--- a/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx
+++ b/src/components/image-editor/ImageCanvasTaskSidebarView.test.tsx
@@ -6,7 +6,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
import { listExternalGenerationTasks } from '../../services/external-generation';
import { ImageCanvasTaskSidebarView } from './ImageCanvasTaskSidebarView';
-import type { CanvasGenerationDialogState } from './ImageCanvasEditorTypes';
vi.mock('../../services/external-generation', () => ({
listExternalGenerationTasks: vi.fn().mockResolvedValue({
@@ -35,27 +34,6 @@ beforeEach(() => {
});
});
-function createDialog(
- overrides: Partial,
-): CanvasGenerationDialogState {
- return {
- id: overrides.id ?? 'dialog-1',
- mode: overrides.mode ?? 'generate',
- prompt: overrides.prompt ?? '',
- status: overrides.status ?? 'idle',
- composerOpen: false,
- placeholder: {
- x: 0,
- y: 0,
- width: 320,
- height: 320,
- originalWidth: 1024,
- originalHeight: 1024,
- },
- ...overrides,
- };
-}
-
function createExternalTask(
overrides: Partial = {},
): ExternalGenerationTaskRecord {
@@ -85,40 +63,53 @@ function createExternalTask(
describe('ImageCanvasTaskSidebarView', () => {
it('groups tasks by tabs and keeps running tasks before queued tasks', async () => {
- const focusGenerationDialog = vi.fn();
+ const focusExternalTask = vi.fn();
const parentWheel = vi.fn();
+ const completedAt = new Date(Date.now() - 1000).toISOString();
+ listExternalGenerationTasksMock.mockImplementation(
+ (options: Parameters[0] = {}) =>
+ Promise.resolve({
+ overview: {
+ pendingCount: 1,
+ runningCount: 1,
+ unacknowledgedTerminalCount: 1,
+ updatedAtMicros: 1,
+ },
+ tasks: options.statuses?.includes('running')
+ ? [
+ createExternalTask({
+ jobId: 'queued-task',
+ requestLabel: '图标素材',
+ status: 'queued',
+ }),
+ createExternalTask({
+ jobId: 'running-task',
+ requestLabel: '角色图片',
+ status: 'running',
+ startedAt: new Date(Date.now() - 3000).toISOString(),
+ }),
+ ]
+ : [
+ createExternalTask({
+ jobId: 'done-task',
+ requestLabel: '视频生成',
+ requestPrompt: '生成一段开场动画',
+ status: 'completed',
+ startedAt: new Date(Date.now() - 4000).toISOString(),
+ completedAt,
+ updatedAt: completedAt,
+ }),
+ ],
+ }),
+ );
+
render(
,
);
@@ -149,11 +140,18 @@ describe('ImageCanvasTaskSidebarView', () => {
.filter((text) => text.includes('角色图片') || text.includes('图标素材'));
expect(activeTitles[0]).toContain('角色图片');
expect(activeTitles[1]).toContain('图标素材');
+ await waitFor(() =>
+ expect(
+ screen
+ .getByRole('tab', { name: /排队\/生成中/u })
+ .getAttribute('aria-selected'),
+ ).toBe('true'),
+ );
const completedTab = screen.getByRole('tab', { name: /已完成/u });
fireEvent.click(completedTab);
- expect(screen.queryByText('视频生成')).not.toBeNull();
- expect(screen.queryByText(/生成一段开场动画/u)).not.toBeNull();
+ expect(await screen.findByText('视频生成')).toBeTruthy();
+ expect(screen.getByText(/生成一段开场动画/u)).toBeTruthy();
expect(screen.queryByText(/完成于/u)).not.toBeNull();
expect(screen.queryByText(/生成完成/u)).toBeNull();
@@ -161,8 +159,8 @@ describe('ImageCanvasTaskSidebarView', () => {
expect(parentWheel).not.toHaveBeenCalled();
fireEvent.click(screen.getByText('视频生成'));
- expect(focusGenerationDialog).toHaveBeenCalledWith(
- expect.objectContaining({ id: 'done-dialog' }),
+ expect(focusExternalTask).toHaveBeenCalledWith(
+ expect.objectContaining({ jobId: 'done-task' }),
);
expect(completedTab.getAttribute('aria-selected')).toBe('true');
});
@@ -216,12 +214,8 @@ describe('ImageCanvasTaskSidebarView', () => {
render(
,
);
@@ -231,9 +225,16 @@ describe('ImageCanvasTaskSidebarView', () => {
).toBeTruthy();
expect(screen.getByText(/已用时 1分/u)).toBeTruthy();
expect(screen.queryByText(/不该显示的外部项目任务/u)).toBeNull();
+ await waitFor(() =>
+ expect(
+ screen
+ .getByRole('tab', { name: /排队\/生成中/u })
+ .getAttribute('aria-selected'),
+ ).toBe('true'),
+ );
fireEvent.click(screen.getByRole('tab', { name: /已完成/u }));
- expect(screen.getByText(/已完成提示词/u)).toBeTruthy();
+ expect(await screen.findByText(/已完成提示词/u)).toBeTruthy();
expect(screen.getByText(/完成于/u)).toBeTruthy();
expect(screen.queryByText(/另一个项目的已完成任务/u)).toBeNull();
});
@@ -266,12 +267,8 @@ describe('ImageCanvasTaskSidebarView', () => {
render(
,
);
diff --git a/src/components/image-editor/ImageCanvasTaskSidebarView.tsx b/src/components/image-editor/ImageCanvasTaskSidebarView.tsx
index 464e6e1c4..5fe705dfc 100644
--- a/src/components/image-editor/ImageCanvasTaskSidebarView.tsx
+++ b/src/components/image-editor/ImageCanvasTaskSidebarView.tsx
@@ -17,11 +17,7 @@ import {
import type { ExternalGenerationTaskRecord } from '../../../packages/shared/src/contracts/externalGeneration';
import { listExternalGenerationTasks } from '../../services/external-generation';
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
-import type {
- CanvasGenerationDialogState,
- CanvasTaskListItem,
- CanvasTaskStatus,
-} from './ImageCanvasEditorTypes';
+import type { CanvasTaskStatus } from './ImageCanvasEditorTypes';
const COMPLETED_TASK_LIST_LIMIT = 20;
const ACTIVE_TASK_LIST_LIMIT = 100;
@@ -44,12 +40,8 @@ type TaskSidebarItem = {
type ImageCanvasTaskSidebarViewProps = {
projectId?: string | null;
- generationDialogs: CanvasGenerationDialogState[];
- backgroundRemovalTasks: CanvasTaskListItem[];
open: boolean;
onToggleOpen: () => void;
- onFocusGenerationDialog: (dialog: CanvasGenerationDialogState) => void;
- onFocusLayer: (layerId: string) => void;
onFocusExternalTask: (task: ExternalGenerationTaskRecord) => void;
};
@@ -85,58 +77,11 @@ function formatTaskTimeMs(timestampMs?: number) {
)}:${pad(date.getMinutes())}`;
}
-function generationDialogTitle(dialog: CanvasGenerationDialogState) {
- switch (dialog.mode) {
- case 'quick-edit':
- return '局部修改';
- case 'character-animation':
- return '角色动画';
- case 'video':
- return '视频生成';
- case 'audio-sound-effect':
- return '音效生成';
- case 'audio-background-music':
- return '背景音乐';
- case 'icon':
- return '图标素材';
- case 'ui-design':
- return 'UI素材提取';
- case 'publication':
- return '宣发素材';
- case 'character':
- return '角色图片';
- case 'spec':
- return '图片规范';
- default:
- return '图片生成';
- }
-}
-
-function generationDialogDetail(dialog: CanvasGenerationDialogState) {
- if (dialog.status === 'generating') {
- return '生成中';
- }
- if (dialog.status === 'failed') {
- return dialog.errorMessage || '生成失败';
- }
- return dialog.generatedLayerId ? '生成完成' : '待提交';
-}
-
function normalizeTaskPrompt(value?: string | null) {
const prompt = value?.trim();
return prompt || null;
}
-function generationDialogTaskDetail(
- dialog: CanvasGenerationDialogState,
- status: CanvasTaskStatus,
-) {
- if (status === 'done') {
- return normalizeTaskPrompt(dialog.prompt) ?? generationDialogTitle(dialog);
- }
- return generationDialogDetail(dialog);
-}
-
function externalTaskDetail(
task: ExternalGenerationTaskRecord,
status: CanvasTaskStatus,
@@ -183,7 +128,7 @@ function canvasTaskStatusFromExternalTask(
function statusLabel(status: CanvasTaskStatus) {
if (status === 'pending') {
- return '待提交';
+ return '排队中';
}
if (status === 'running') {
return '进行中';
@@ -267,12 +212,8 @@ function mergeExternalTaskRefresh(
export function ImageCanvasTaskSidebarView({
projectId,
- generationDialogs,
- backgroundRemovalTasks,
open,
onToggleOpen,
- onFocusGenerationDialog,
- onFocusLayer,
onFocusExternalTask,
}: ImageCanvasTaskSidebarViewProps) {
const [activeTab, setActiveTab] = useState('active');
@@ -280,7 +221,6 @@ export function ImageCanvasTaskSidebarView({
const [externalTasks, setExternalTasks] = useState<
ExternalGenerationTaskRecord[]
>([]);
- const observedStartedAtRef = useRef(new Map());
const previousActiveItemCountRef = useRef(0);
const filterVisibleExternalTasks = useCallback(
(tasks: ExternalGenerationTaskRecord[]) =>
@@ -395,13 +335,16 @@ export function ImageCanvasTaskSidebarView({
window.clearTimeout(timerId);
}
};
- }, [activeExternalTaskIds, activeExternalTaskKey, filterVisibleExternalTasks]);
+ }, [
+ activeExternalTaskIds,
+ activeExternalTaskKey,
+ filterVisibleExternalTasks,
+ ]);
useEffect(() => {
- const hasRunningTask =
- generationDialogs.some((dialog) => dialog.status === 'generating') ||
- backgroundRemovalTasks.some((task) => task.status === 'running') ||
- externalTasks.some((task) => task.status === 'running');
+ const hasRunningTask = externalTasks.some(
+ (task) => task.status === 'running',
+ );
if (!hasRunningTask) {
return undefined;
}
@@ -409,66 +352,9 @@ export function ImageCanvasTaskSidebarView({
setNow(Date.now());
}, 1000);
return () => window.clearInterval(timerId);
- }, [backgroundRemovalTasks, externalTasks, generationDialogs]);
+ }, [externalTasks]);
const items = useMemo(() => {
- const resolveStartedAt = (itemId: string, startedAt?: number) => {
- if (typeof startedAt === 'number' && Number.isFinite(startedAt)) {
- return startedAt;
- }
- const observedStartedAt = observedStartedAtRef.current.get(itemId);
- if (observedStartedAt) {
- return observedStartedAt;
- }
- observedStartedAtRef.current.set(itemId, now);
- return now;
- };
- const generationItems: TaskSidebarItem[] = generationDialogs.map(
- (dialog) => {
- const status =
- dialog.status === 'generating'
- ? 'running'
- : dialog.status === 'failed'
- ? 'failed'
- : dialog.generatedLayerId
- ? 'done'
- : 'pending';
- const id = `generation-${dialog.id}`;
- const startedAt =
- status === 'running'
- ? resolveStartedAt(id, dialog.generationStartedAt)
- : dialog.generationStartedAt;
- const finishedAt = dialog.generationFinishedAt;
- return {
- id,
- title: generationDialogTitle(dialog),
- detail: generationDialogTaskDetail(dialog, status),
- status,
- startedAt,
- finishedAt,
- elapsedMs:
- status === 'running' && startedAt
- ? now - startedAt
- : startedAt && finishedAt
- ? finishedAt - startedAt
- : undefined,
- errorMessage: dialog.errorMessage,
- onClick: () => onFocusGenerationDialog(dialog),
- sortTime: finishedAt ?? startedAt ?? 0,
- };
- },
- );
- const backgroundItems: TaskSidebarItem[] = backgroundRemovalTasks.map(
- (task) => ({
- ...task,
- elapsedMs:
- task.status === 'running' ? now - task.startedAt : task.elapsedMs,
- onClick: task.focusLayerId
- ? () => onFocusLayer(task.focusLayerId!)
- : undefined,
- sortTime: task.finishedAt ?? task.startedAt,
- }),
- );
const externalVisibleTasks = mergeExternalTasks(
externalTasks.filter(
(task) => task.status === 'queued' || task.status === 'running',
@@ -515,16 +401,8 @@ export function ImageCanvasTaskSidebarView({
0,
};
});
- return [...backgroundItems, ...generationItems, ...externalItems];
- }, [
- backgroundRemovalTasks,
- externalTasks,
- generationDialogs,
- now,
- onFocusExternalTask,
- onFocusGenerationDialog,
- onFocusLayer,
- ]);
+ return externalItems;
+ }, [externalTasks, now, onFocusExternalTask]);
const activeItems = useMemo(
() =>
@@ -597,7 +475,10 @@ export function ImageCanvasTaskSidebarView({
任务列表
{items.length}
-