- Introduced `generationCancelJob` command ID in `commands/ids.ts`. - Added `GenerationCancelJobPayload` type in `commands/payloads.ts`. - Enhanced job status to include "cancelled" in `editor/state.ts`. - Updated `runGenerationJob` to accept an `AbortSignal` and handle cancellation. - Implemented cancellation logic in `runGenerate` and related functions. - Added tests for job cancellation in `operations/generation/workflow.test.ts`. - Improved layer rendering logic to prevent stack overflow in `editor/document-indexes.ts`. - Added raster size assertions in `platform/browser/rasterLimits.ts` for image processing limits. - Enhanced image file handling to check for size limits in `platform/browser/imageFiles.ts`. - Updated UI components to reflect job cancellation state in `view/GenerationJobStatus.tsx` and `view/bottom-controls/GenerateActionControls.tsx`.
30 lines
1.6 KiB
TypeScript
30 lines
1.6 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import type { GenerationJob, GenerationState } from "@editor/state";
|
|
|
|
export function currentGenerationJob(generation: GenerationState): GenerationJob | undefined {
|
|
return generation.jobs.find((job) => job.status === "running") ?? generation.jobs[0];
|
|
}
|
|
|
|
export function GenerationJobStatus({ generation, compact = false }: { generation: GenerationState; compact?: boolean }) {
|
|
const job = currentGenerationJob(generation);
|
|
const [, setTick] = useState(0);
|
|
|
|
useEffect(() => {
|
|
if (job?.status !== "running") return;
|
|
const interval = window.setInterval(() => setTick((tick) => tick + 1), 1000);
|
|
return () => window.clearInterval(interval);
|
|
}, [job?.id, job?.status]);
|
|
|
|
if (!job) return null;
|
|
const elapsed = Math.max(0, Math.floor(((job.finishedAt ?? Date.now()) - job.startedAt) / 1000));
|
|
const label = job.status === "running" ? `${job.label} ${formatElapsed(elapsed)}` : job.status === "failed" ? job.error ?? `${job.label} failed` : job.status === "cancelled" ? `${job.label} cancelled` : `${job.label} complete`;
|
|
const tone = job.status === "failed" ? "bg-red-500/15 text-red-100" : job.status === "running" ? "bg-white/10 text-white/70" : job.status === "cancelled" ? "bg-amber-500/15 text-amber-100" : "bg-emerald-500/15 text-emerald-100";
|
|
|
|
return <span className={`${compact ? "max-w-56" : "max-w-80"} truncate rounded-full px-3 py-2 text-xs font-medium ${tone}`} title={label} aria-live="polite">{label}</span>;
|
|
}
|
|
|
|
function formatElapsed(seconds: number) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
return `${minutes}:${(seconds % 60).toString().padStart(2, "0")}`;
|
|
}
|