progress indicator added
This commit is contained in:
+139
-4
@@ -29,10 +29,60 @@ import {
|
||||
type GenerateConfig,
|
||||
type TextAlign,
|
||||
} from "@/lib/id-config";
|
||||
import {
|
||||
isProgressEvent,
|
||||
type GenerateProgressEvent,
|
||||
} from "@/lib/progress";
|
||||
|
||||
const IMAGE_ACCEPT = "image/png,image/jpeg,image/webp";
|
||||
const FONT_ACCEPT = ".ttf,.otf,font/ttf,font/otf";
|
||||
|
||||
async function readGenerateProgress(
|
||||
response: Response,
|
||||
onEvent: (event: Extract<GenerateProgressEvent, { phase: "generate" | "zip" }>) => void,
|
||||
): Promise<string> {
|
||||
if (!response.body) {
|
||||
throw new Error("No progress stream from server");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let jobId: string | null = null;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() ?? "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(line);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!isProgressEvent(parsed)) continue;
|
||||
if (parsed.phase === "error") {
|
||||
throw new Error(parsed.message);
|
||||
}
|
||||
if (parsed.phase === "done") {
|
||||
jobId = parsed.id;
|
||||
continue;
|
||||
}
|
||||
onEvent(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
if (!jobId) {
|
||||
throw new Error("Generation finished without a download");
|
||||
}
|
||||
return jobId;
|
||||
}
|
||||
|
||||
function canvasPoint(
|
||||
canvas: HTMLCanvasElement,
|
||||
clientX: number,
|
||||
@@ -60,6 +110,11 @@ export function Editor() {
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [status, setStatus] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useState<{
|
||||
phase: "generate" | "zip" | "download";
|
||||
current: number;
|
||||
total: number;
|
||||
} | null>(null);
|
||||
const [cursor, setCursor] = useState<"grab" | "grabbing" | "default">(
|
||||
"default",
|
||||
);
|
||||
@@ -298,7 +353,8 @@ export function Editor() {
|
||||
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
setStatus(`Generating ${config.count} images on the server…`);
|
||||
setStatus(null);
|
||||
setProgress({ phase: "generate", current: 0, total: config.count });
|
||||
|
||||
try {
|
||||
const form = new FormData();
|
||||
@@ -324,7 +380,23 @@ export function Editor() {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const jobId = await readGenerateProgress(response, (event) => {
|
||||
if (event.phase === "generate" || event.phase === "zip") {
|
||||
setProgress({
|
||||
phase: event.phase,
|
||||
current: event.current,
|
||||
total: event.total,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setProgress({ phase: "download", current: 1, total: 1 });
|
||||
const zipResponse = await fetch(`/api/download/${jobId}`);
|
||||
if (!zipResponse.ok) {
|
||||
throw new Error("Could not download the zip file");
|
||||
}
|
||||
|
||||
const blob = await zipResponse.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
@@ -333,8 +405,10 @@ export function Editor() {
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setProgress(null);
|
||||
setStatus(`Downloaded ids.zip with ${config.count} PNGs`);
|
||||
} catch (err) {
|
||||
setProgress(null);
|
||||
setStatus(null);
|
||||
setError(err instanceof Error ? err.message : "Generation failed");
|
||||
} finally {
|
||||
@@ -686,7 +760,8 @@ export function Editor() {
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
{status && !error ? (
|
||||
{progress ? <ProgressPanel progress={progress} /> : null}
|
||||
{status && !error && !progress ? (
|
||||
<p className="rounded-lg border border-indigo-900 bg-indigo-950/50 px-3 py-2 text-sm text-indigo-100">
|
||||
{status}
|
||||
</p>
|
||||
@@ -698,7 +773,9 @@ export function Editor() {
|
||||
disabled={busy || !imageFile}
|
||||
className="rounded-xl bg-indigo-500 px-4 py-3 text-sm font-semibold text-white transition hover:bg-indigo-400 disabled:cursor-not-allowed disabled:bg-zinc-700 disabled:text-zinc-400"
|
||||
>
|
||||
{busy ? "Generating…" : `Generate ${config.count} images`}
|
||||
{busy
|
||||
? progressLabel(progress, config.count)
|
||||
: `Generate ${config.count} images`}
|
||||
</button>
|
||||
</aside>
|
||||
</div>
|
||||
@@ -706,6 +783,64 @@ export function Editor() {
|
||||
);
|
||||
}
|
||||
|
||||
function progressLabel(
|
||||
progress: {
|
||||
phase: "generate" | "zip" | "download";
|
||||
current: number;
|
||||
total: number;
|
||||
} | null,
|
||||
fallbackTotal: number,
|
||||
): string {
|
||||
if (!progress) return `Generating 0 / ${fallbackTotal}`;
|
||||
if (progress.phase === "zip") {
|
||||
return `Zipping ${progress.current} / ${progress.total}`;
|
||||
}
|
||||
if (progress.phase === "download") {
|
||||
return "Downloading zip…";
|
||||
}
|
||||
return `Generating ${progress.current} / ${progress.total}`;
|
||||
}
|
||||
|
||||
function ProgressPanel({
|
||||
progress,
|
||||
}: {
|
||||
progress: {
|
||||
phase: "generate" | "zip" | "download";
|
||||
current: number;
|
||||
total: number;
|
||||
};
|
||||
}) {
|
||||
const percent =
|
||||
progress.total > 0
|
||||
? Math.min(100, Math.round((progress.current / progress.total) * 100))
|
||||
: 0;
|
||||
const title =
|
||||
progress.phase === "zip"
|
||||
? "Zipping"
|
||||
: progress.phase === "download"
|
||||
? "Downloading"
|
||||
: "Generating";
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-indigo-900 bg-indigo-950/50 px-3 py-3 text-sm text-indigo-100">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span>{title}</span>
|
||||
<span className="font-mono text-xs text-indigo-200">
|
||||
{progress.phase === "download"
|
||||
? "ids.zip"
|
||||
: `${progress.current} / ${progress.total}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-zinc-800">
|
||||
<div
|
||||
className="h-full rounded-full bg-indigo-400 transition-[width] duration-150"
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FieldGroup({
|
||||
title,
|
||||
children,
|
||||
|
||||
Reference in New Issue
Block a user