Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions frontend/src/components/ui/ApiError.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div role="alert" aria-live="assertive" className="w-full border border-red-500/40 bg-red-900/20 text-red-200 px-4 py-3 rounded-lg flex items-start justify-between gap-3">
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 mt-0.5" />

<p className="text-sm break-words">{message}</p>
</div>

<button
onClick={onDismiss}
aria-label="Dismiss error"
className="cursor-pointer hover:text-red-400 transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
);
}
15 changes: 11 additions & 4 deletions frontend/src/pages/NewAnalysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -44,6 +45,7 @@ export default function NewAnalysis() {
const [busy, setBusy] = useState(false)
const [resultRun, setResultRun] = useState<Run | null>(null)
const [resultPayload, setResultPayload] = useState<Record<string, unknown> | null>(null)
const [error, setError] = useState<string | null>(null)

const canSubmit = bbox !== null && startDate && endDate && !busy

Expand All @@ -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,
Expand All @@ -85,16 +90,18 @@ 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)
}
}

return (
<div className="max-w-4xl mx-auto px-6 py-8 space-y-8">

<ApiError message={error} onDismiss={() => setError(null)} />

{/* Step 1 — Analysis Type */}
<section>
<SectionLabel step={1} label="Analysis Type" />
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/pages/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string | null>(null)
const [uploadProgress, setUploadProgress] = useState<number | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)

Expand All @@ -57,6 +59,7 @@ export default function Upload() {
const handleUpload = async () => {
if (!file) return
setBusy(true)
setError(null);
setUploadProgress(0)

try {
Expand All @@ -75,17 +78,18 @@ 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)
}
}

return (
<div className="max-w-3xl mx-auto px-6 py-8 space-y-6">

<ApiError message={error} onDismiss={() => setError(null)} />
{/* Drop Zone */}
<div
onDragOver={(e) => { e.preventDefault(); setDragging(true) }}
Expand Down