progress indicator added

This commit is contained in:
2026-09-13 15:30:23 +05:30
parent 9c7a16b57c
commit 13c24e9db3
5 changed files with 319 additions and 69 deletions
+139 -4
View File
@@ -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,