Files
guilan-multimedia-lab/frontend/src/App.jsx

189 lines
7.1 KiB
JavaScript

import { useEffect, useMemo, useState } from "react";
import { Toaster, toast } from "sonner";
import CanvasPane, { CanvasThumbnail } from "./components/CanvasPane.jsx";
import Controls from "./components/Controls.jsx";
import HistogramPanel from "./components/HistogramPanel.jsx";
import { applyStateOperation, combineStates, deleteState, getOperations, listStates, uploadImage } from "./lib/api.js";
function showErrorToast(error, fallback = "Something went wrong.") {
const message = error?.message || fallback;
toast.error(message, {
description: "Please check the selected image, operation, and parameters."
});
return message;
}
export default function App() {
const [session, setSession] = useState(null);
const [states, setStates] = useState([]);
const [activeState, setActiveState] = useState(null);
const [operations, setOperations] = useState([]);
const [selectedOperation, setSelectedOperation] = useState("");
const [params, setParams] = useState({});
const [selectedStateIds, setSelectedStateIds] = useState([]);
const [status, setStatus] = useState("Upload an image to begin.");
const [busy, setBusy] = useState(false);
const [transform, setTransform] = useState({ x: 0, y: 0, scale: 1 });
useEffect(() => {
getOperations()
.then((payload) => {
setOperations(payload.operations || []);
const first = payload.operations?.[0];
if (first) {
setSelectedOperation(first.id);
setParams({ _repeat: 1, ...Object.fromEntries(Object.entries(first.params || {}).map(([key, schema]) => [key, schema.default])) });
}
})
.catch((error) => setStatus(showErrorToast(error, "Unable to load operations.")));
}, []);
async function refreshStates(sessionId, nextActiveId = null) {
const payload = await listStates(sessionId);
setStates(payload.states || []);
const nextActive = payload.states?.find((state) => state.state_id === nextActiveId) || payload.states?.at(-1) || null;
setActiveState(nextActive);
return nextActive;
}
async function handleUpload(file) {
if (!file) return;
setBusy(true);
setStatus("Uploading image...");
try {
const payload = await uploadImage(file);
setSession(payload);
const initialStates = payload.states || [];
setStates(initialStates);
setActiveState(initialStates[0] || null);
setSelectedStateIds(initialStates[0] ? [initialStates[0].state_id] : []);
setTransform({ x: 0, y: 0, scale: 1 });
setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded as S0.`);
} catch (error) {
setStatus(showErrorToast(error, "Unable to upload image."));
} finally {
setBusy(false);
}
}
function handleSelectOperation(operationId, nextParams) {
setSelectedOperation(operationId);
setParams(nextParams);
}
function handleParamChange(key, value) {
setParams((current) => ({ ...current, [key]: value }));
}
async function handleApply() {
if (!activeState || !selectedOperation) return;
setBusy(true);
setStatus(`Applying ${selectedOperation} to ${activeState.label}...`);
try {
const state = await applyStateOperation(activeState.state_id, selectedOperation, params);
await refreshStates(state.session_id, state.state_id);
setSelectedStateIds([state.state_id]);
setStatus(`${state.label} created.`);
} catch (error) {
setStatus(showErrorToast(error, "Unable to apply operation."));
} finally {
setBusy(false);
}
}
async function handleCombine(kind) {
if (selectedStateIds.length < 2) return;
setBusy(true);
setStatus(`Combining ${selectedStateIds.length} states using ${kind}...`);
try {
const state = await combineStates(kind, selectedStateIds);
await refreshStates(state.session_id, state.state_id);
setSelectedStateIds([state.state_id]);
setStatus(`${state.label} created.`);
} catch (error) {
setStatus(showErrorToast(error, "Unable to combine states."));
} finally {
setBusy(false);
}
}
function toggleCombineState(stateId) {
setSelectedStateIds((current) => current.includes(stateId) ? current.filter((id) => id !== stateId) : [...current, stateId]);
}
async function handleDeleteState(state) {
if (!state || !session) return;
setBusy(true);
setStatus(`Deleting ${state.label}...`);
try {
await deleteState(state.state_id);
const remaining = await listStates(session.session_id);
const nextStates = remaining.states || [];
setStates(nextStates);
const currentActive = nextStates.find((item) => item.state_id === activeState?.state_id);
const fallback = activeState?.state_id === state.state_id ? nextStates.at(-1) || nextStates[0] || null : currentActive || nextStates.at(-1) || nextStates[0] || null;
setActiveState(fallback);
setSelectedStateIds((current) => current.filter((id) => id !== state.state_id));
setStatus(`${state.label} deleted.`);
} catch (error) {
setStatus(showErrorToast(error, "Unable to delete state."));
} finally {
setBusy(false);
}
}
const originalState = states[0] || null;
const viewportTitle = useMemo(() => {
if (!activeState) return "No active state";
return `${activeState.label} - ${activeState.width} x ${activeState.height} ${activeState.color_mode}`;
}, [activeState]);
return (
<div className="flex h-screen overflow-hidden bg-zinc-950 text-zinc-100">
<Toaster richColors theme="dark" position="top-right" closeButton />
<Controls
operations={operations}
selectedOperation={selectedOperation}
params={params}
states={states}
activeState={activeState}
selectedStateIds={selectedStateIds}
busy={busy}
onUpload={handleUpload}
onSelectOperation={handleSelectOperation}
onParamChange={handleParamChange}
onApply={handleApply}
onSelectState={(state) => {
setActiveState(state);
}}
onToggleCombineState={toggleCombineState}
onCombine={handleCombine}
onDeleteState={handleDeleteState}
/>
<main className="flex h-screen min-w-0 flex-1 flex-col overflow-hidden">
<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">Professor Slide Workspace</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="relative min-h-0 flex-1 overflow-hidden bg-zinc-800">
<CanvasThumbnail title="S0 Original" imageData={originalState?.image_data} />
<CanvasPane
title="Active State"
imageData={activeState?.image_data}
histogram={activeState?.histogram}
transform={transform}
onTransform={setTransform}
/>
</div>
<HistogramPanel original={originalState?.histogram} processed={activeState?.histogram} />
</main>
</div>
);
}