Use time-driven generation progress
Web Quality / verify (push) Has been cancelled

This commit is contained in:
2026-06-10 16:00:26 +08:00
parent 9e080bbb8f
commit e9601a651c
10 changed files with 390 additions and 28 deletions
+6
View File
@@ -33,6 +33,10 @@ import { communityClient } from "../../api/communityClient";
import { modelCapabilitiesClient } from "../../api/modelCapabilitiesClient";
import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway";
import WorkspacePageShell from "../../components/WorkspacePageShell";
import {
DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS,
DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS,
} from "../../hooks/useSmoothedProgress";
import type { WebCanvasWorkflow } from "../../types";
import type { AssetLibraryCategory } from "../assets/localAssetStore";
import {
@@ -4492,6 +4496,7 @@ function CanvasPage({
progress={imageNodeProgress}
status={imageTaskState?.status || "running"}
message={imageTaskState?.message || "图片生成中"}
expectedDurationMs={DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS}
/>
) : null}
{imageNodeFocusActive && imageFocusSelectionReady ? (
@@ -4866,6 +4871,7 @@ function CanvasPage({
progress={videoNodeProgress}
status={videoTaskState?.status || "running"}
message={videoTaskState?.message || "视频生成中"}
expectedDurationMs={DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS}
/>
) : null}
{renderConnectorButton({ kind: "video", nodeId: videoNode.id, side: "left", slot: "center" }, "studio-canvas-video-node__connector")}
@@ -1,4 +1,8 @@
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
import {
DEFAULT_GENERATION_EXPECTED_DURATION_MS,
useSmoothedProgress,
type ProgressSource,
} from "../../hooks/useSmoothedProgress";
import { canvasGenerationProgressStyle } from "./canvasUtils";
type NodeGenStatus = "submitting" | "running" | "success" | "error";
@@ -7,10 +11,24 @@ interface CanvasSmoothedProgressRingProps {
progress: number;
status: NodeGenStatus;
message?: string;
progressSource?: ProgressSource;
startedAt?: number | string | Date | null;
expectedDurationMs?: number | null;
}
export function CanvasSmoothedProgressRing({ progress, status, message }: CanvasSmoothedProgressRingProps) {
const smoothed = useSmoothedProgress(progress, status);
export function CanvasSmoothedProgressRing({
progress,
status,
message,
progressSource = "estimated",
startedAt,
expectedDurationMs = DEFAULT_GENERATION_EXPECTED_DURATION_MS,
}: CanvasSmoothedProgressRingProps) {
const smoothed = useSmoothedProgress(progress, status, {
progressSource,
startedAt,
expectedDurationMs,
});
return (
<div
className="studio-canvas-node-generation-progress"
@@ -18,7 +36,10 @@ export function CanvasSmoothedProgressRing({ progress, status, message }: Canvas
aria-live="polite"
style={canvasGenerationProgressStyle(smoothed)}
>
<span className="studio-canvas-node-generation-progress__ring" aria-hidden="true" />
<span
className="studio-canvas-node-generation-progress__ring"
aria-hidden="true"
/>
<strong>{message}</strong>
<em>{smoothed}%</em>
</div>
+22 -2
View File
@@ -262,7 +262,17 @@ export async function waitForImageTaskResult(
abortRef,
onProgress: (e) => {
if (onStatus) {
onStatus({ taskId, status: e.status, progress: e.progress, resultUrl: e.resultUrl ?? undefined, error: e.error ?? undefined } as AiTaskStatus);
onStatus({
taskId,
status: e.status,
progress: e.progress,
progressSource: e.progressSource,
stage: e.stage,
startedAt: e.startedAt,
expectedDurationMs: e.expectedDurationMs,
resultUrl: e.resultUrl ?? undefined,
error: e.error ?? undefined,
} as AiTaskStatus);
}
},
});
@@ -281,7 +291,17 @@ export async function waitForVideoTaskResult(
abortRef,
onProgress: (e) => {
if (onStatus) {
onStatus({ taskId, status: e.status, progress: e.progress, resultUrl: e.resultUrl ?? undefined, error: e.error ?? undefined } as AiTaskStatus);
onStatus({
taskId,
status: e.status,
progress: e.progress,
progressSource: e.progressSource,
stage: e.stage,
startedAt: e.startedAt,
expectedDurationMs: e.expectedDurationMs,
resultUrl: e.resultUrl ?? undefined,
error: e.error ?? undefined,
} as AiTaskStatus);
}
},
});
@@ -1,4 +1,9 @@
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
import { useRef } from "react";
import {
DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS,
formatEstimatedRemainingLabel,
useSmoothedProgress,
} from "../../hooks/useSmoothedProgress";
interface EcommerceProgressBarProps {
status: "idle" | "generating" | "done" | "failed" | string;
@@ -12,19 +17,42 @@ function mapStatus(status: string): "running" | "completed" | "failed" {
return "running";
}
export function EcommerceProgressBar({ status, label }: EcommerceProgressBarProps) {
const progress = mapStatus(status) === "running" ? 50 : 100;
const smoothed = useSmoothedProgress(progress, mapStatus(status));
export function EcommerceProgressBar({
status,
label,
}: EcommerceProgressBarProps) {
const startedAtRef = useRef(Date.now());
const mappedStatus = mapStatus(status);
const progress = mappedStatus === "running" ? 50 : 100;
const smoothed = useSmoothedProgress(progress, mappedStatus, {
progressSource: mappedStatus === "running" ? "estimated" : "real",
startedAt: startedAtRef.current,
expectedDurationMs: DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS,
});
const remainingLabel =
mappedStatus === "running"
? formatEstimatedRemainingLabel({
nowMs: Date.now(),
startedAt: startedAtRef.current,
expectedDurationMs: DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS,
})
: null;
if (status === "idle") return null;
return (
<div className="ecommerce-progress-bar">
<span className="ecommerce-progress-bar__label">{label || "AI 正在生成"}</span>
<span className="ecommerce-progress-bar__label">
{label || "AI 正在生成"}
{remainingLabel ? ` / ${remainingLabel}` : ""}
</span>
<div className="ecommerce-progress-bar__track">
<div className="ecommerce-progress-bar__fill" style={{ width: `${smoothed}%` }} />
<div
className="ecommerce-progress-bar__fill"
style={{ width: `${smoothed}%` }}
/>
</div>
<span className="ecommerce-progress-bar__value">{smoothed}%</span>
</div>
);
}
}
+21 -3
View File
@@ -1,4 +1,8 @@
import { useSmoothedProgress } from "../../hooks/useSmoothedProgress";
import {
DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS,
useSmoothedProgress,
type ProgressSource,
} from "../../hooks/useSmoothedProgress";
type MessageStatus = "thinking" | "completed" | "failed" | string;
@@ -6,6 +10,9 @@ interface SmoothedProgressBarProps {
progress: number;
status: MessageStatus;
label?: string;
progressSource?: ProgressSource;
startedAt?: number | string | Date | null;
expectedDurationMs?: number | null;
}
function mapMessageStatus(status: MessageStatus) {
@@ -15,8 +22,19 @@ function mapMessageStatus(status: MessageStatus) {
return "running" as const;
}
export function SmoothedProgressBar({ progress, status, label }: SmoothedProgressBarProps) {
const smoothed = useSmoothedProgress(progress, mapMessageStatus(status));
export function SmoothedProgressBar({
progress,
status,
label,
progressSource = "estimated",
startedAt,
expectedDurationMs = DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS,
}: SmoothedProgressBarProps) {
const smoothed = useSmoothedProgress(progress, mapMessageStatus(status), {
progressSource,
startedAt,
expectedDurationMs,
});
return (
<>
<span>{label || "超分处理中..."}</span>
@@ -19,7 +19,13 @@ import { assetClient } from "../../../api/assetClient";
import { communityClient } from "../../../api/communityClient";
import { saveAssetToLocalLibrary } from "../../assets/localAssetStore";
import { SmoothedProgressBar } from "../SmoothedProgressBar";
import { useSmoothedProgress } from "../../../hooks/useSmoothedProgress";
import {
DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS,
DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS,
DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS,
formatEstimatedRemainingLabel,
useSmoothedProgress,
} from "../../../hooks/useSmoothedProgress";
import { renderMarkdownBlocks } from "../markdownRenderer";
import { downloadResultAsset } from "../workbenchDownload";
import type { WorkbenchChatAttachment, WorkbenchChatMessage, WorkbenchResultActionPayload } from "../workbenchChatTypes";
@@ -456,6 +462,8 @@ export const ResultCard = memo(function ResultCard({
progress={message.taskProgress ?? 18}
status={message.status || "thinking"}
label={message.taskStatusLabel || "超分处理中..."}
progressSource="estimated"
expectedDurationMs={DEFAULT_SUPER_RESOLUTION_EXPECTED_DURATION_MS}
/>
</div>
) : null}
@@ -575,7 +583,23 @@ export const GenerationPendingCard = memo(function GenerationPendingCard({
const specs = message.result?.specs || [];
const prompt = message.prompt || message.body;
const isVideo = message.mode === "video";
const smoothed = useSmoothedProgress(message.taskProgress ?? 5, message.status === "thinking" ? "running" : "completed");
const expectedDurationMs = isVideo
? DEFAULT_VIDEO_GENERATION_EXPECTED_DURATION_MS
: DEFAULT_IMAGE_GENERATION_EXPECTED_DURATION_MS;
const progressStatus = message.status === "thinking" ? "running" : "completed";
const smoothed = useSmoothedProgress(message.taskProgress ?? 5, progressStatus, {
progressSource: progressStatus === "running" ? "estimated" : "real",
startedAt: message.createdAt,
expectedDurationMs,
});
const remainingLabel = progressStatus === "running"
? formatEstimatedRemainingLabel({
nowMs: Date.now(),
startedAt: message.createdAt,
expectedDurationMs,
})
: null;
const statusLabel = message.taskStatusLabel || (isVideo ? "视频生成中" : "图片生成中");
return (
<div className={`ai-generation-pending-card${isVideo ? " is-video" : " is-image"}`}>
@@ -590,7 +614,7 @@ export const GenerationPendingCard = memo(function GenerationPendingCard({
</div>
<div className="ai-generation-pending-card__meta">
<div>
<strong>{message.taskStatusLabel || "Generating..."}</strong>
<strong>{remainingLabel ? `${statusLabel} / ${remainingLabel}` : statusLabel}</strong>
<span>{prompt}</span>
</div>
{specs.length > 0 && (