feat(v5): add user friendly error toasts

This commit is contained in:
2026-07-09 19:53:55 +08:00
parent 30e1d250ef
commit da390b8bd9
4 changed files with 45 additions and 7 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 (
<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}

View File

@@ -1,9 +1,25 @@
const API_BASE = import.meta.env.VITE_API_BASE || "";
function humanizeErrorDetail(detail) {
if (!detail) return "Request failed";
if (typeof detail === "string") return detail;
if (Array.isArray(detail)) return detail.map(humanizeErrorDetail).join(" ");
if (typeof detail === "object") {
return Object.entries(detail)
.map(([key, value]) => {
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;
}