diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 45b7dfd..68909d4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "react-dom": "latest", "react-quick-pinch-zoom": "latest", "recharts": "latest", + "sonner": "^2.0.7", "vite": "latest" }, "devDependencies": { @@ -3027,6 +3028,16 @@ "dev": true, "license": "ISC" }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://package-mirror.liara.ir/repository/npm/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://package-mirror.liara.ir/repository/npm/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index f4d46a0..ffb53e5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,8 @@ "react-quick-pinch-zoom": "latest", "recharts": "latest", "lucide-react": "latest", - "prop-types": "latest" + "prop-types": "latest", + "sonner": "^2.0.7" }, "devDependencies": { "tailwindcss": "3.4.17", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4c0b079..6b04d35 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,9 +1,18 @@ 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([]); @@ -26,7 +35,7 @@ export default function App() { setParams({ _repeat: 1, ...Object.fromEntries(Object.entries(first.params || {}).map(([key, schema]) => [key, schema.default])) }); } }) - .catch((error) => setStatus(error.message)); + .catch((error) => setStatus(showErrorToast(error, "Unable to load operations."))); }, []); async function refreshStates(sessionId, nextActiveId = null) { @@ -51,7 +60,7 @@ export default function App() { setTransform({ x: 0, y: 0, scale: 1 }); setStatus(`${payload.width} x ${payload.height} ${payload.color_mode} image loaded as S0.`); } catch (error) { - setStatus(error.message); + setStatus(showErrorToast(error, "Unable to upload image.")); } finally { setBusy(false); } @@ -76,7 +85,7 @@ export default function App() { setSelectedStateIds([state.state_id]); setStatus(`${state.label} created.`); } catch (error) { - setStatus(error.message); + setStatus(showErrorToast(error, "Unable to apply operation.")); } finally { setBusy(false); } @@ -92,7 +101,7 @@ export default function App() { setSelectedStateIds([state.state_id]); setStatus(`${state.label} created.`); } catch (error) { - setStatus(error.message); + setStatus(showErrorToast(error, "Unable to combine states.")); } finally { setBusy(false); } @@ -117,7 +126,7 @@ export default function App() { setSelectedStateIds((current) => current.filter((id) => id !== state.state_id)); setStatus(`${state.label} deleted.`); } catch (error) { - setStatus(error.message); + setStatus(showErrorToast(error, "Unable to delete state.")); } finally { setBusy(false); } @@ -131,6 +140,7 @@ export default function App() { return (
+ { + const message = humanizeErrorDetail(value); + return key === "non_field_errors" || key === "detail" ? message : `${key}: ${message}`; + }) + .filter(Boolean) + .join(" "); + } + return String(detail); +} + async function parseResponse(response) { const payload = await response.json().catch(() => ({})); if (!response.ok) { - throw new Error(payload.detail || "Request failed"); + throw new Error(humanizeErrorDetail(payload.detail || payload)); } return payload; }