diff --git a/frontend/src/components/ui/ApiError.tsx b/frontend/src/components/ui/ApiError.tsx new file mode 100644 index 0000000..3aca085 --- /dev/null +++ b/frontend/src/components/ui/ApiError.tsx @@ -0,0 +1,29 @@ +import { AlertCircle, X } from "lucide-react"; + + +interface ApiErrorProps { + message: string | null; + onDismiss: () => void; +} + +export function ApiError({ message, onDismiss }: ApiErrorProps) { + if (!message) return null; + + return ( +
+
+ + +

{message}

+
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/pages/NewAnalysis.tsx b/frontend/src/pages/NewAnalysis.tsx index a670bc8..bb23acf 100644 --- a/frontend/src/pages/NewAnalysis.tsx +++ b/frontend/src/pages/NewAnalysis.tsx @@ -10,6 +10,7 @@ import { ErrorBoundary } from '../components/ui/ErrorBoundary' import { useToast } from '../contexts/ToastContext' import { useApp } from '../contexts/AppContext' import type { Run } from '../api' +import { ApiError } from '../components/ui/ApiError' const PRESETS = [ { label: 'Last 30d', days: 30 }, @@ -44,6 +45,7 @@ export default function NewAnalysis() { const [busy, setBusy] = useState(false) const [resultRun, setResultRun] = useState(null) const [resultPayload, setResultPayload] = useState | null>(null) + const [error, setError] = useState(null) const canSubmit = bbox !== null && startDate && endDate && !busy @@ -62,13 +64,16 @@ export default function NewAnalysis() { return } - setBusy(true) + setResultRun(null) setResultPayload(null) try { + setBusy(true); + setError(null); const res = await predictJson({ kind: 'bbox', analysis_type: analysisType, bbox: bbox!, start_date: startDate, end_date: endDate }) setResultPayload(res.result) + // Construct a minimal Run object for the results panel setResultRun({ id: res.run_id, @@ -85,8 +90,9 @@ export default function NewAnalysis() { label: 'View in history', onClick: () => navigate('/runs'), }) - } catch (e) { - showToast('error', String(e)) + } catch (e:any) { + const message = e?.response?.data?.detail || e?.message || "Something went wrong"; + setError(message); } finally { setBusy(false) } @@ -94,7 +100,8 @@ export default function NewAnalysis() { return (
- + setError(null)} /> + {/* Step 1 — Analysis Type */}
diff --git a/frontend/src/pages/Upload.tsx b/frontend/src/pages/Upload.tsx index 5107689..7ba2be5 100644 --- a/frontend/src/pages/Upload.tsx +++ b/frontend/src/pages/Upload.tsx @@ -8,6 +8,7 @@ import { MapBBoxPicker } from '../components/Map/MapBBoxPicker' import { ErrorBoundary } from '../components/ui/ErrorBoundary' import { useToast } from '../contexts/ToastContext' import { useApp } from '../contexts/AppContext' +import { ApiError } from '../components/ui/ApiError' const ACCEPTED = ['.tif', '.tiff', '.geotiff', '.nc', '.hdf5'] const MAX_MB = 500 @@ -31,6 +32,7 @@ export default function Upload() { const [startDate, setStartDate] = useState('') const [endDate, setEndDate] = useState('') const [busy, setBusy] = useState(false) + const [error, setError] = useState(null) const [uploadProgress, setUploadProgress] = useState(null) const fileInputRef = useRef(null) @@ -57,6 +59,7 @@ export default function Upload() { const handleUpload = async () => { if (!file) return setBusy(true) + setError(null); setUploadProgress(0) try { @@ -75,9 +78,10 @@ export default function Upload() { }) setFile(null) setUploadProgress(null) - } catch (e) { - showToast('error', String(e)) - setUploadProgress(null) + } catch (e:any) { + const message = e?.response?.data?.detail || e?.message || "Upload failed"; + setError(message); + setUploadProgress(null); } finally { setBusy(false) } @@ -85,7 +89,7 @@ export default function Upload() { return (
- + setError(null)} /> {/* Drop Zone */}
{ e.preventDefault(); setDragging(true) }}