feat(v1): add basic backend and frontend
This commit is contained in:
152
frontend/src/App.jsx
Normal file
152
frontend/src/App.jsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import CanvasPane from "./components/CanvasPane.jsx";
|
||||
import Controls from "./components/Controls.jsx";
|
||||
import HistogramPanel from "./components/HistogramPanel.jsx";
|
||||
import { createBatch, getJob, processImage, uploadImage } from "./lib/api.js";
|
||||
import { useDebouncedEffect } from "./lib/debounce.js";
|
||||
|
||||
export default function App() {
|
||||
const [session, setSession] = useState(null);
|
||||
const [processed, setProcessed] = useState(null);
|
||||
const [batchSessions, setBatchSessions] = useState([]);
|
||||
const [operation, setOperation] = useState("gamma");
|
||||
const [params, setParams] = useState({ gamma: 1 });
|
||||
const [status, setStatus] = useState("Upload an image to begin.");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
|
||||
|
||||
const originalHistogram = session?.original_histogram;
|
||||
const processedHistogram = processed?.processed_histogram || processed?.result_histogram;
|
||||
|
||||
async function handleUpload(file) {
|
||||
if (!file) return;
|
||||
setBusy(true);
|
||||
setStatus("Uploading image...");
|
||||
try {
|
||||
const payload = await uploadImage(file);
|
||||
setSession(payload);
|
||||
setProcessed(null);
|
||||
setBatchSessions([payload.session_id]);
|
||||
setTransform({ x: 0, y: 0, scale: 1 });
|
||||
setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBatchUpload(file) {
|
||||
if (!file) return;
|
||||
setBusy(true);
|
||||
setStatus("Uploading batch image...");
|
||||
try {
|
||||
const payload = await uploadImage(file);
|
||||
setBatchSessions((current) => [...current, payload.session_id]);
|
||||
setStatus("Batch image added.");
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function runBatch(kind) {
|
||||
setBusy(true);
|
||||
setStatus(`Starting ${kind} job...`);
|
||||
try {
|
||||
const job = await createBatch(kind, batchSessions);
|
||||
const result = await pollJob(job.job_id);
|
||||
setProcessed(result);
|
||||
setStatus(`${kind} complete.`);
|
||||
} catch (error) {
|
||||
setStatus(error.message);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function pollJob(jobId) {
|
||||
for (let attempt = 0; attempt < 80; attempt += 1) {
|
||||
const job = await getJob(jobId);
|
||||
setStatus(`Job ${job.status}: ${job.progress}%`);
|
||||
if (job.status === "complete") return job;
|
||||
if (job.status === "failed") throw new Error(job.error || "Batch job failed");
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 1000));
|
||||
}
|
||||
throw new Error("Batch job timed out.");
|
||||
}
|
||||
|
||||
useDebouncedEffect(
|
||||
() => {
|
||||
if (!session?.session_id || !operation) return;
|
||||
let cancelled = false;
|
||||
async function run() {
|
||||
setBusy(true);
|
||||
setStatus(`Processing ${operation}...`);
|
||||
try {
|
||||
const payload = await processImage(session.session_id, operation, params);
|
||||
if (!cancelled) {
|
||||
setProcessed(payload);
|
||||
setStatus(`${operation} complete in ${payload.elapsed_ms} ms.`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (!cancelled) setStatus(error.message);
|
||||
} finally {
|
||||
if (!cancelled) setBusy(false);
|
||||
}
|
||||
}
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
},
|
||||
[session?.session_id, operation, JSON.stringify(params)],
|
||||
300
|
||||
);
|
||||
|
||||
const processedImage = processed?.image_data || session?.image_data;
|
||||
const originalImage = session?.image_data;
|
||||
|
||||
const viewportTitle = useMemo(() => {
|
||||
if (!session) return "No image";
|
||||
return `${session.width} x ${session.height} ${session.color_mode}`;
|
||||
}, [session]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-zinc-950 text-zinc-100 lg:flex-row">
|
||||
<Controls
|
||||
selected={operation}
|
||||
params={params}
|
||||
onOperationChange={(nextOperation, nextParams) => {
|
||||
setOperation(nextOperation);
|
||||
setParams(nextParams);
|
||||
}}
|
||||
onParamChange={(key, value) => setParams((current) => ({ ...current, [key]: value }))}
|
||||
onUpload={handleUpload}
|
||||
onBatchUpload={handleBatchUpload}
|
||||
batchCount={batchSessions.length}
|
||||
onBatchRun={runBatch}
|
||||
disabled={!session || busy}
|
||||
busy={busy}
|
||||
/>
|
||||
|
||||
<main className="flex min-h-0 flex-1 flex-col">
|
||||
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-zinc-800 bg-zinc-950 px-5 py-3">
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-cyan-300">Professional Dark Studio</p>
|
||||
<h2 className="text-sm font-medium text-zinc-200">{viewportTitle}</h2>
|
||||
</div>
|
||||
<div className="text-sm text-zinc-400">{busy ? "Working..." : status}</div>
|
||||
</header>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-1 gap-px bg-zinc-800 lg:grid-cols-2">
|
||||
<CanvasPane title="Original" imageData={originalImage} histogram={originalHistogram} transform={transform} onTransform={setTransform} />
|
||||
<CanvasPane title="Processed" imageData={processedImage} histogram={processedHistogram} transform={transform} onTransform={setTransform} />
|
||||
</div>
|
||||
|
||||
<HistogramPanel original={originalHistogram} processed={processedHistogram} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user