Professor Slide Workspace
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 (
Professor Slide Workspace