IngSeb0 commited on
Commit
2200d51
·
1 Parent(s): a5d76a0

Deploy Next.js frontend Docker Space

Browse files
Dockerfile ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-bookworm-slim
2
+
3
+ WORKDIR /app
4
+
5
+ ENV NODE_ENV=production
6
+ ENV NEXT_TELEMETRY_DISABLED=1
7
+ ENV PORT=7860
8
+ ENV HOSTNAME=0.0.0.0
9
+
10
+ # Public backend URL on AMD Developer Cloud.
11
+ # This is public, not a secret.
12
+ ARG NEXT_PUBLIC_API_BASE_URL=http://134.199.203.136:8080
13
+ ENV NEXT_PUBLIC_API_BASE_URL=${NEXT_PUBLIC_API_BASE_URL}
14
+
15
+ COPY frontend/package*.json ./frontend/
16
+
17
+ WORKDIR /app/frontend
18
+
19
+ RUN if [ -f package-lock.json ]; then npm ci; else npm install; fi
20
+
21
+ COPY frontend/ ./
22
+
23
+ RUN npm run build
24
+
25
+ EXPOSE 7860
26
+
27
+ CMD ["npx", "next", "start", "-H", "0.0.0.0", "-p", "7860"]
README.md CHANGED
@@ -1,10 +1,20 @@
1
  ---
2
- title: ReliefLens Frontend
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: green
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: ReliefLens AI Frontend
3
+ emoji: 🚨
4
+ colorFrom: blue
5
+ colorTo: red
6
  sdk: docker
7
+ app_port: 7860
8
  ---
9
 
10
+ # ReliefLens AI Frontend
11
+
12
+ This Docker Space runs the Next.js frontend for ReliefLens AI.
13
+
14
+ The backend runs on AMD Developer Cloud:
15
+
16
+ `http://134.199.203.136:8080`
17
+
18
+ ## Safety
19
+
20
+ This demo uses synthetic disaster-response data. It is not connected to emergency services and must not be used for real-world dispatch decisions.
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/app/favicon.ico ADDED
frontend/app/globals.css ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ --background: #030712;
5
+ --foreground: #f9fafb;
6
+ }
7
+
8
+ body {
9
+ background: var(--background);
10
+ color: var(--foreground);
11
+ font-family: system-ui, -apple-system, sans-serif;
12
+ }
13
+
14
+ /* Custom scrollbar */
15
+ ::-webkit-scrollbar {
16
+ width: 6px;
17
+ height: 6px;
18
+ }
19
+ ::-webkit-scrollbar-track {
20
+ background: #111827;
21
+ }
22
+ ::-webkit-scrollbar-thumb {
23
+ background: #374151;
24
+ border-radius: 3px;
25
+ }
26
+ ::-webkit-scrollbar-thumb:hover {
27
+ background: #4b5563;
28
+ }
29
+
frontend/app/layout.tsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import "./globals.css";
3
+
4
+ export const metadata: Metadata = {
5
+ title: "ReliefLensAI | Crisis Room",
6
+ description:
7
+ "Real-time multimodal disaster triage system powered by AMD MI300X",
8
+ };
9
+
10
+ export default function RootLayout({
11
+ children,
12
+ }: {
13
+ children: React.ReactNode;
14
+ }) {
15
+ return (
16
+ <html lang="es" className="h-full">
17
+ <body className="bg-gray-950 text-white antialiased min-h-screen">
18
+ {children}
19
+ </body>
20
+ </html>
21
+ );
22
+ }
frontend/app/page.tsx ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useMemo, useState } from "react";
4
+ import { Activity, AlertTriangle, RefreshCw, ShieldCheck, Wifi, WifiOff } from "lucide-react";
5
+ import { AMDPanel } from "@/components/AMDPanel";
6
+ import { IncidentCard } from "@/components/IncidentCard";
7
+ import { StatsBar } from "@/components/StatsBar";
8
+ import { UploadPanel } from "@/components/UploadPanel";
9
+ import { api } from "@/lib/api";
10
+ import type {
11
+ AMDPerformanceMetric,
12
+ CrisisRoomSummary,
13
+ DispatchMessage,
14
+ Incident,
15
+ Priority,
16
+ ResourceRecommendation,
17
+ } from "@/lib/types";
18
+
19
+ type PriorityFilter = "ALL" | Priority;
20
+
21
+ const FILTERS: PriorityFilter[] = ["ALL", "P0", "P1", "P2", "P3"];
22
+
23
+ export default function CrisisRoomPage() {
24
+ const [backendOnline, setBackendOnline] = useState<boolean | null>(null);
25
+ const [processing, setProcessing] = useState(false);
26
+ const [sessionId, setSessionId] = useState<string | null>(null);
27
+ const [scenarioName, setScenarioName] = useState<string>("Sin sesión activa");
28
+ const [summary, setSummary] = useState<CrisisRoomSummary | null>(null);
29
+ const [incidents, setIncidents] = useState<Incident[]>([]);
30
+ const [resources, setResources] = useState<ResourceRecommendation[]>([]);
31
+ const [dispatchMessages, setDispatchMessages] = useState<DispatchMessage[]>([]);
32
+ const [amdMetrics, setAmdMetrics] = useState<AMDPerformanceMetric | null>(null);
33
+ const [filter, setFilter] = useState<PriorityFilter>("ALL");
34
+ const [lastError, setLastError] = useState<string | null>(null);
35
+
36
+ const fetchHealth = useCallback(async () => {
37
+ try {
38
+ await api.getHealth();
39
+ setBackendOnline(true);
40
+ } catch {
41
+ setBackendOnline(false);
42
+ }
43
+ }, []);
44
+
45
+ const fetchAmdMetrics = useCallback(async () => {
46
+ try {
47
+ const res = await api.getAMDMetrics();
48
+ setAmdMetrics(res.data as AMDPerformanceMetric);
49
+ } catch {
50
+ setAmdMetrics(null);
51
+ }
52
+ }, []);
53
+
54
+ const fetchIncidents = useCallback(async (sid: string) => {
55
+ const res = await api.getIncidents(sid);
56
+ setIncidents(Array.isArray(res.data) ? (res.data as Incident[]) : []);
57
+ }, []);
58
+
59
+ useEffect(() => {
60
+ fetchHealth();
61
+ fetchAmdMetrics();
62
+ const interval = setInterval(fetchAmdMetrics, 5000);
63
+ return () => clearInterval(interval);
64
+ }, [fetchAmdMetrics, fetchHealth]);
65
+
66
+ const filteredIncidents = useMemo(
67
+ () => (filter === "ALL" ? incidents : incidents.filter((incident) => incident.priority === filter)),
68
+ [filter, incidents],
69
+ );
70
+
71
+ const counts = useMemo(
72
+ () => ({
73
+ total: incidents.length,
74
+ p0: incidents.filter((incident) => incident.priority === "P0").length,
75
+ p1: incidents.filter((incident) => incident.priority === "P1").length,
76
+ p2: incidents.filter((incident) => incident.priority === "P2").length,
77
+ p3: incidents.filter((incident) => incident.priority === "P3").length,
78
+ }),
79
+ [incidents],
80
+ );
81
+
82
+ const resourcesByIncident = useMemo(() => {
83
+ const map = new Map<string, ResourceRecommendation[]>();
84
+ for (const resource of resources) {
85
+ const bucket = map.get(resource.incident_id) ?? [];
86
+ bucket.push(resource);
87
+ map.set(resource.incident_id, bucket);
88
+ }
89
+ return map;
90
+ }, [resources]);
91
+
92
+ const dispatchByIncident = useMemo(() => {
93
+ const map = new Map<string, DispatchMessage>();
94
+ for (const message of dispatchMessages) {
95
+ if (!map.has(message.incident_id)) {
96
+ map.set(message.incident_id, message);
97
+ }
98
+ }
99
+ return map;
100
+ }, [dispatchMessages]);
101
+
102
+ function handleResults(nextSummary: CrisisRoomSummary) {
103
+ setSummary(nextSummary);
104
+ setSessionId(nextSummary.session_id);
105
+ setScenarioName(nextSummary.scenario_name);
106
+ setResources(nextSummary.resource_recommendations ?? []);
107
+ setDispatchMessages(nextSummary.dispatch_messages ?? []);
108
+ setAmdMetrics(nextSummary.amd_metrics ?? null);
109
+ setLastError(null);
110
+ fetchIncidents(nextSummary.session_id).catch(() => {
111
+ setLastError("No se pudo refrescar la lista de incidentes de la sesión.");
112
+ });
113
+ }
114
+
115
+ async function handleRefresh() {
116
+ await fetchHealth();
117
+ await fetchAmdMetrics();
118
+ if (sessionId) {
119
+ try {
120
+ await fetchIncidents(sessionId);
121
+ } catch {
122
+ setLastError("No se pudo actualizar la sesión actual.");
123
+ }
124
+ }
125
+ }
126
+
127
+ return (
128
+ <div className="min-h-screen bg-gray-950 text-white">
129
+ <header className="border-b border-gray-800 bg-gray-950/95 backdrop-blur">
130
+ <div className="mx-auto flex max-w-[1600px] items-center justify-between gap-4 px-4 py-4">
131
+ <div className="flex items-center gap-3">
132
+ <div className="rounded-lg border border-red-900/60 bg-red-950/40 p-2">
133
+ <AlertTriangle className="h-5 w-5 text-red-400" />
134
+ </div>
135
+ <div>
136
+ <p className="text-xs uppercase tracking-[0.2em] text-gray-500">ReliefLens AI</p>
137
+ <h1 className="text-lg font-semibold text-white">Crisis Room</h1>
138
+ </div>
139
+ </div>
140
+
141
+ <div className="flex items-center gap-2">
142
+ <span
143
+ className={`inline-flex items-center gap-2 rounded-md border px-3 py-2 text-xs ${
144
+ backendOnline
145
+ ? "border-emerald-900/60 bg-emerald-950/40 text-emerald-300"
146
+ : backendOnline === false
147
+ ? "border-red-900/60 bg-red-950/40 text-red-300"
148
+ : "border-gray-800 bg-gray-900 text-gray-400"
149
+ }`}
150
+ >
151
+ {backendOnline ? <Wifi className="h-3.5 w-3.5" /> : <WifiOff className="h-3.5 w-3.5" />}
152
+ {backendOnline ? "Backend online" : backendOnline === false ? "Backend offline" : "Verificando"}
153
+ </span>
154
+ <span className="inline-flex items-center gap-2 rounded-md border border-orange-900/60 bg-orange-950/40 px-3 py-2 text-xs text-orange-300">
155
+ <Activity className="h-3.5 w-3.5" />
156
+ AMD MI300X
157
+ </span>
158
+ <button
159
+ onClick={handleRefresh}
160
+ className="inline-flex items-center gap-2 rounded-md border border-gray-800 bg-gray-900 px-3 py-2 text-xs text-gray-300 transition-colors hover:bg-gray-800"
161
+ >
162
+ <RefreshCw className="h-3.5 w-3.5" />
163
+ Refresh
164
+ </button>
165
+ </div>
166
+ </div>
167
+ </header>
168
+
169
+ <main className="mx-auto grid max-w-[1600px] gap-6 px-4 py-6 xl:grid-cols-[360px_minmax(0,1fr)_340px]">
170
+ <div className="space-y-4">
171
+ <section className="rounded-xl border border-gray-800 bg-gray-900 p-4">
172
+ <h2 className="text-sm font-semibold text-white">Ingesta</h2>
173
+ <p className="mt-1 text-sm text-gray-400">Demo guiada y prueba rápida para validar el backend.</p>
174
+ <div className="mt-4">
175
+ <UploadPanel
176
+ processing={processing}
177
+ onProcessing={setProcessing}
178
+ onError={setLastError}
179
+ onResults={(nextSummary) => handleResults(nextSummary)}
180
+ />
181
+ </div>
182
+ </section>
183
+
184
+ <section className="rounded-xl border border-gray-800 bg-gray-900 p-4">
185
+ <h2 className="text-sm font-semibold text-white">Sesión activa</h2>
186
+ <dl className="mt-3 space-y-3 text-sm">
187
+ <div>
188
+ <dt className="text-gray-500">Escenario</dt>
189
+ <dd className="mt-1 text-gray-200">{scenarioName}</dd>
190
+ </div>
191
+ <div>
192
+ <dt className="text-gray-500">Session ID</dt>
193
+ <dd className="mt-1 break-all font-mono text-xs text-gray-300">{sessionId ?? "—"}</dd>
194
+ </div>
195
+ <div>
196
+ <dt className="text-gray-500">Estado</dt>
197
+ <dd className="mt-1 text-gray-200">{summary?.status ?? "idle"}</dd>
198
+ </div>
199
+ <div>
200
+ <dt className="text-gray-500">Tiempo de proceso</dt>
201
+ <dd className="mt-1 text-gray-200">
202
+ {summary ? `${summary.processing_time_seconds.toFixed(2)} s` : "—"}
203
+ </dd>
204
+ </div>
205
+ </dl>
206
+ </section>
207
+ </div>
208
+
209
+ <div className="space-y-6">
210
+ <StatsBar total={counts.total} p0={counts.p0} p1={counts.p1} p2={counts.p2} p3={counts.p3} />
211
+
212
+ <section className="rounded-xl border border-gray-800 bg-gray-900 p-4">
213
+ <div className="flex flex-wrap items-center justify-between gap-3">
214
+ <div>
215
+ <h2 className="text-sm font-semibold text-white">Operational Incidents</h2>
216
+ <p className="mt-1 text-sm text-gray-400">
217
+ {filteredIncidents.length} visibles de {incidents.length} incidentes consolidados.
218
+ </p>
219
+ </div>
220
+ <div className="flex items-center gap-2">
221
+ {FILTERS.map((option) => (
222
+ <button
223
+ key={option}
224
+ onClick={() => setFilter(option)}
225
+ className={`rounded-md px-3 py-1.5 text-xs font-semibold transition-colors ${
226
+ filter === option ? "bg-white text-gray-950" : "bg-gray-950 text-gray-400 hover:bg-gray-800 hover:text-white"
227
+ }`}
228
+ >
229
+ {option === "ALL" ? "Todos" : option}
230
+ </button>
231
+ ))}
232
+ </div>
233
+ </div>
234
+
235
+ {lastError ? (
236
+ <div className="mt-4 rounded-lg border border-red-900/60 bg-red-950/30 px-3 py-2 text-sm text-red-300">
237
+ {lastError}
238
+ </div>
239
+ ) : null}
240
+
241
+ <div className="mt-4 space-y-3">
242
+ {filteredIncidents.length === 0 ? (
243
+ <div className="rounded-xl border border-dashed border-gray-800 bg-gray-950/40 px-6 py-12 text-center">
244
+ <p className="text-sm font-medium text-gray-300">Sin incidentes cargados</p>
245
+ <p className="mt-1 text-sm text-gray-500">
246
+ Ejecuta la demo o procesa mensajes de texto desde el panel izquierdo.
247
+ </p>
248
+ </div>
249
+ ) : (
250
+ filteredIncidents.map((incident) => (
251
+ <IncidentCard
252
+ key={incident.id}
253
+ incident={incident}
254
+ resources={resourcesByIncident.get(incident.id) ?? []}
255
+ dispatch={dispatchByIncident.get(incident.id) ?? null}
256
+ onApprove={(id) =>
257
+ setIncidents((prev) =>
258
+ prev.map((current) => (current.id === id ? { ...current, human_approved: true } : current)),
259
+ )
260
+ }
261
+ />
262
+ ))
263
+ )}
264
+ </div>
265
+ </section>
266
+ </div>
267
+
268
+ <div className="space-y-4">
269
+ <AMDPanel metrics={amdMetrics} />
270
+
271
+ <section className="rounded-xl border border-gray-800 bg-gray-900 p-4">
272
+ <div className="flex items-center gap-2">
273
+ <ShieldCheck className="h-4 w-4 text-emerald-400" />
274
+ <h2 className="text-sm font-semibold text-white">Guardrails</h2>
275
+ </div>
276
+ <ul className="mt-3 space-y-2 text-sm text-gray-400">
277
+ <li>Los incidentes P0 y P1 requieren aprobación humana antes de despacho.</li>
278
+ <li>La evidencia multimodal se mantiene vinculada al incidente consolidado.</li>
279
+ <li>Este sistema apoya coordinación; no sustituye respuesta médica ni emergencias oficiales.</li>
280
+ </ul>
281
+ </section>
282
+
283
+ <section className="rounded-xl border border-gray-800 bg-gray-900 p-4">
284
+ <h2 className="text-sm font-semibold text-white">Dispatch readiness</h2>
285
+ <dl className="mt-3 grid grid-cols-2 gap-3 text-sm">
286
+ <div className="rounded-lg border border-gray-800 bg-gray-950/60 p-3">
287
+ <dt className="text-gray-500">Mensajes listos</dt>
288
+ <dd className="mt-1 font-mono text-xl text-white">{dispatchMessages.length}</dd>
289
+ </div>
290
+ <div className="rounded-lg border border-gray-800 bg-gray-950/60 p-3">
291
+ <dt className="text-gray-500">Recursos activos</dt>
292
+ <dd className="mt-1 font-mono text-xl text-white">{resources.length}</dd>
293
+ </div>
294
+ </dl>
295
+ </section>
296
+ </div>
297
+ </main>
298
+ </div>
299
+ );
300
+ }
frontend/components/AMDPanel.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Activity, Cpu, MemoryStick, Zap } from "lucide-react";
4
+ import type { AMDPerformanceMetric } from "@/lib/types";
5
+
6
+ interface AMDPanelProps {
7
+ metrics: AMDPerformanceMetric | null;
8
+ loading?: boolean;
9
+ }
10
+
11
+ function ProgressBar({
12
+ value,
13
+ max = 100,
14
+ tone,
15
+ }: {
16
+ value: number;
17
+ max?: number;
18
+ tone: string;
19
+ }) {
20
+ const pct = Math.min(100, Math.round((value / max) * 100));
21
+ return (
22
+ <div className="h-2 overflow-hidden rounded-full bg-gray-800">
23
+ <div className={`h-2 rounded-full transition-all duration-500 ${tone}`} style={{ width: `${pct}%` }} />
24
+ </div>
25
+ );
26
+ }
27
+
28
+ export function AMDPanel({ metrics, loading }: AMDPanelProps) {
29
+ return (
30
+ <section className="rounded-xl border border-gray-800 bg-gray-900 p-4">
31
+ <div className="flex items-center justify-between gap-3">
32
+ <div className="flex items-center gap-2">
33
+ <Cpu className="h-4 w-4 text-orange-400" />
34
+ <h3 className="text-sm font-semibold text-white">AMD Performance</h3>
35
+ </div>
36
+ <span className="text-xs text-gray-500">{loading ? "Actualizando" : "Telemetría activa"}</span>
37
+ </div>
38
+
39
+ <div className="mt-3 rounded-lg border border-orange-900/50 bg-orange-950/40 px-3 py-2 text-xs text-orange-300">
40
+ {metrics?.model_name ?? "Modelo no disponible"}{metrics?.rocm_version ? ` · ROCm ${metrics.rocm_version}` : ""}
41
+ </div>
42
+
43
+ <div className="mt-4 space-y-4">
44
+ <div>
45
+ <div className="mb-1 flex items-center justify-between text-xs text-gray-400">
46
+ <span className="flex items-center gap-1.5">
47
+ <Activity className="h-3.5 w-3.5" />
48
+ Utilización GPU
49
+ </span>
50
+ <span className="font-mono text-orange-300">
51
+ {metrics ? `${metrics.gpu_utilization.toFixed(1)}%` : "—"}
52
+ </span>
53
+ </div>
54
+ <ProgressBar value={metrics?.gpu_utilization ?? 0} tone="bg-orange-500" />
55
+ </div>
56
+
57
+ <div>
58
+ <div className="mb-1 flex items-center justify-between text-xs text-gray-400">
59
+ <span className="flex items-center gap-1.5">
60
+ <MemoryStick className="h-3.5 w-3.5" />
61
+ Memoria HBM
62
+ </span>
63
+ <span className="font-mono text-purple-300">
64
+ {metrics ? `${metrics.memory_used_gb.toFixed(1)} / ${metrics.memory_total_gb.toFixed(0)} GB` : "—"}
65
+ </span>
66
+ </div>
67
+ <ProgressBar
68
+ value={metrics?.memory_used_gb ?? 0}
69
+ max={metrics?.memory_total_gb ?? 1}
70
+ tone="bg-purple-500"
71
+ />
72
+ </div>
73
+ </div>
74
+
75
+ <div className="mt-4 grid grid-cols-2 gap-2">
76
+ <div className="rounded-lg border border-gray-800 bg-gray-950/70 p-3">
77
+ <p className="flex items-center gap-1.5 text-xs text-gray-500">
78
+ <Zap className="h-3.5 w-3.5 text-yellow-400" />
79
+ Tokens/s
80
+ </p>
81
+ <p className="mt-1 font-mono text-xl font-semibold text-white">
82
+ {metrics ? metrics.tokens_per_second.toFixed(0) : "—"}
83
+ </p>
84
+ </div>
85
+ <div className="rounded-lg border border-gray-800 bg-gray-950/70 p-3">
86
+ <p className="text-xs text-gray-500">Latencia media</p>
87
+ <p className="mt-1 font-mono text-xl font-semibold text-white">
88
+ {metrics ? `${metrics.avg_latency_ms.toFixed(0)} ms` : "—"}
89
+ </p>
90
+ </div>
91
+ <div className="rounded-lg border border-gray-800 bg-gray-950/70 p-3">
92
+ <p className="text-xs text-gray-500">Requests</p>
93
+ <p className="mt-1 font-mono text-lg font-semibold text-white">
94
+ {metrics?.requests_processed ?? "—"}
95
+ </p>
96
+ </div>
97
+ <div className="rounded-lg border border-gray-800 bg-gray-950/70 p-3">
98
+ <p className="text-xs text-gray-500">Potencia</p>
99
+ <p className="mt-1 font-mono text-lg font-semibold text-white">
100
+ {metrics?.power_watts ? `${metrics.power_watts.toFixed(0)} W` : "—"}
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </section>
105
+ );
106
+ }
frontend/components/IncidentCard.tsx ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { CheckCircle2, ChevronDown, ChevronUp, FileText, Image, MapPin, Mic, Radio, Send, Table, Users } from "lucide-react";
5
+ import { PriorityBadge } from "@/components/PriorityBadge";
6
+ import { api } from "@/lib/api";
7
+ import type { DispatchMessage, EvidenceItem, Incident, ResourceRecommendation } from "@/lib/types";
8
+
9
+ const MODALITY_ICON: Record<string, React.ReactNode> = {
10
+ text: <FileText className="h-3.5 w-3.5 text-blue-400" />,
11
+ image: <Image className="h-3.5 w-3.5 text-purple-400" />,
12
+ audio: <Mic className="h-3.5 w-3.5 text-emerald-400" />,
13
+ csv: <Table className="h-3.5 w-3.5 text-yellow-400" />,
14
+ location: <MapPin className="h-3.5 w-3.5 text-cyan-400" />,
15
+ };
16
+
17
+ const STATUS_LABELS: Record<string, string> = {
18
+ new: "Nuevo",
19
+ acknowledged: "Recibido",
20
+ in_progress: "En progreso",
21
+ resolved: "Resuelto",
22
+ };
23
+
24
+ interface IncidentCardProps {
25
+ incident: Incident;
26
+ resources?: ResourceRecommendation[];
27
+ dispatch?: DispatchMessage | null;
28
+ onApprove?: (id: string) => void;
29
+ }
30
+
31
+ function EvidenceRow({ item }: { item: EvidenceItem }) {
32
+ return (
33
+ <div className="flex items-start gap-2 rounded-lg border border-gray-800 bg-gray-950/70 px-3 py-2">
34
+ <span className="mt-0.5 shrink-0">{MODALITY_ICON[item.modality] ?? MODALITY_ICON.text}</span>
35
+ <span className="text-xs leading-5 text-gray-300">{item.description}</span>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export function IncidentCard({ incident, resources = [], dispatch, onApprove }: IncidentCardProps) {
41
+ const [expanded, setExpanded] = useState(false);
42
+ const [approved, setApproved] = useState(incident.human_approved);
43
+ const [dispatchMessage, setDispatchMessage] = useState(dispatch?.message ?? "");
44
+ const [generatingDispatch, setGeneratingDispatch] = useState(false);
45
+
46
+ const confidencePct = Math.round(incident.confidence * 100);
47
+ const statusLabel = STATUS_LABELS[incident.status] ?? incident.status;
48
+ const resourceSummary = useMemo(
49
+ () =>
50
+ resources
51
+ .map((resource) =>
52
+ resource.quantity ? `${resource.quantity} ${resource.description}` : resource.description,
53
+ )
54
+ .slice(0, 3)
55
+ .join(" · "),
56
+ [resources],
57
+ );
58
+
59
+ async function handleApprove() {
60
+ try {
61
+ await api.approveIncident(incident.id);
62
+ } catch {
63
+ // optimistic update
64
+ }
65
+ setApproved(true);
66
+ onApprove?.(incident.id);
67
+ }
68
+
69
+ async function handleGenerateDispatch() {
70
+ setGeneratingDispatch(true);
71
+ try {
72
+ const res = await api.generateDispatch(incident.id);
73
+ setDispatchMessage(res.data?.message ?? "");
74
+ } finally {
75
+ setGeneratingDispatch(false);
76
+ }
77
+ }
78
+
79
+ return (
80
+ <article className="overflow-hidden rounded-xl border border-gray-800 bg-gray-900">
81
+ <button
82
+ onClick={() => setExpanded((value) => !value)}
83
+ className="flex w-full items-start gap-3 p-4 text-left"
84
+ aria-expanded={expanded}
85
+ >
86
+ <div className="shrink-0">
87
+ <PriorityBadge priority={incident.priority} />
88
+ </div>
89
+
90
+ <div className="min-w-0 flex-1">
91
+ <div className="flex items-start justify-between gap-4">
92
+ <div className="min-w-0">
93
+ <h3 className="truncate text-sm font-semibold text-white">{incident.title}</h3>
94
+ <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-400">
95
+ <span className="flex items-center gap-1">
96
+ <MapPin className="h-3 w-3" />
97
+ {incident.location ?? "Ubicación no disponible"}
98
+ </span>
99
+ <span className="flex items-center gap-1">
100
+ <Users className="h-3 w-3" />
101
+ {incident.affected_people ?? 0} personas
102
+ </span>
103
+ <span>{statusLabel}</span>
104
+ </div>
105
+ </div>
106
+
107
+ <div className="flex items-center gap-2">
108
+ <span className="font-mono text-xs text-gray-500">{confidencePct}%</span>
109
+ {approved ? <CheckCircle2 className="h-4 w-4 text-emerald-400" /> : null}
110
+ {expanded ? <ChevronUp className="h-4 w-4 text-gray-500" /> : <ChevronDown className="h-4 w-4 text-gray-500" />}
111
+ </div>
112
+ </div>
113
+
114
+ {!expanded && resourceSummary ? (
115
+ <p className="mt-2 truncate text-xs text-gray-500">{resourceSummary}</p>
116
+ ) : null}
117
+ </div>
118
+ </button>
119
+
120
+ {expanded ? (
121
+ <div className="border-t border-gray-800 px-4 pb-4 pt-4">
122
+ <div className="grid gap-4 lg:grid-cols-[1.3fr_0.7fr]">
123
+ <div className="space-y-4">
124
+ <section>
125
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-gray-500">Situación</h4>
126
+ <p className="mt-2 text-sm leading-6 text-gray-300">{incident.description}</p>
127
+ </section>
128
+
129
+ <section>
130
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-gray-500">
131
+ Evidencia ({incident.evidence.length})
132
+ </h4>
133
+ <div className="mt-2 space-y-2">
134
+ {incident.evidence.map((item) => (
135
+ <EvidenceRow key={item.id} item={item} />
136
+ ))}
137
+ </div>
138
+ </section>
139
+ </div>
140
+
141
+ <div className="space-y-4">
142
+ <section className="rounded-lg border border-gray-800 bg-gray-950/70 p-3">
143
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-gray-500">Recursos sugeridos</h4>
144
+ {resources.length > 0 ? (
145
+ <div className="mt-2 space-y-2">
146
+ {resources.map((resource) => (
147
+ <div key={resource.id} className="rounded-md border border-gray-800 bg-gray-900 px-3 py-2">
148
+ <p className="text-sm font-medium text-white">
149
+ {resource.quantity ? `${resource.quantity} ` : ""}
150
+ {resource.description}
151
+ </p>
152
+ <p className="mt-1 text-xs text-gray-400">
153
+ {resource.urgency} · {resource.rationale}
154
+ </p>
155
+ </div>
156
+ ))}
157
+ </div>
158
+ ) : (
159
+ <p className="mt-2 text-xs text-gray-500">Sin recursos asociados en esta sesión.</p>
160
+ )}
161
+ </section>
162
+
163
+ <section className="rounded-lg border border-gray-800 bg-gray-950/70 p-3">
164
+ <div className="flex items-center justify-between gap-3">
165
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-gray-500">
166
+ Mensaje de despacho
167
+ </h4>
168
+ <button
169
+ onClick={handleGenerateDispatch}
170
+ disabled={generatingDispatch}
171
+ className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-blue-500 disabled:opacity-60"
172
+ >
173
+ <Send className="h-3.5 w-3.5" />
174
+ {generatingDispatch ? "Generando" : "Generar"}
175
+ </button>
176
+ </div>
177
+ <div className="mt-2 rounded-md border border-gray-800 bg-gray-900 p-3 text-xs leading-5 text-gray-300">
178
+ {dispatchMessage || "Todavía no hay mensaje generado para este incidente."}
179
+ </div>
180
+ </section>
181
+
182
+ <div className="flex items-center justify-between gap-3">
183
+ <div className="inline-flex items-center gap-2 rounded-md border border-gray-800 bg-gray-950/70 px-3 py-2 text-xs text-gray-400">
184
+ <Radio className="h-3.5 w-3.5 text-orange-400" />
185
+ Human-in-the-loop requerido para despacho crítico
186
+ </div>
187
+ <button
188
+ onClick={handleApprove}
189
+ disabled={approved}
190
+ className={`inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-semibold transition-colors ${
191
+ approved ? "bg-emerald-900/50 text-emerald-300" : "bg-emerald-600 text-white hover:bg-emerald-500"
192
+ }`}
193
+ >
194
+ <CheckCircle2 className="h-4 w-4" />
195
+ {approved ? "Aprobado" : "Aprobar"}
196
+ </button>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ ) : null}
202
+ </article>
203
+ );
204
+ }
frontend/components/PriorityBadge.tsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Priority } from "@/lib/types";
2
+
3
+ const PRIORITY_CONFIG: Record<Priority, { bg: string; text: string; label: string }> = {
4
+ P0: { bg: "bg-red-600/90", text: "text-white", label: "P0" },
5
+ P1: { bg: "bg-orange-500/90", text: "text-white", label: "P1" },
6
+ P2: { bg: "bg-yellow-500/90", text: "text-gray-950", label: "P2" },
7
+ P3: { bg: "bg-emerald-500/90", text: "text-white", label: "P3" },
8
+ };
9
+
10
+ export function PriorityBadge({ priority }: { priority: Priority }) {
11
+ const cfg = PRIORITY_CONFIG[priority];
12
+
13
+ return (
14
+ <span className={`inline-flex min-w-11 items-center justify-center rounded-md px-2.5 py-1 text-xs font-bold ${cfg.bg} ${cfg.text}`}>
15
+ {cfg.label}
16
+ </span>
17
+ );
18
+ }
frontend/components/StatsBar.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ interface StatsBarProps {
4
+ total: number;
5
+ p0: number;
6
+ p1: number;
7
+ p2: number;
8
+ p3: number;
9
+ }
10
+
11
+ function StatTile({
12
+ label,
13
+ value,
14
+ accent,
15
+ tone,
16
+ }: {
17
+ label: string;
18
+ value: number;
19
+ accent: string;
20
+ tone: string;
21
+ }) {
22
+ return (
23
+ <div className={`rounded-xl border p-4 ${accent}`}>
24
+ <p className="text-xs uppercase tracking-wide text-gray-500">{label}</p>
25
+ <p className={`mt-2 font-mono text-3xl font-semibold ${tone}`}>{value}</p>
26
+ </div>
27
+ );
28
+ }
29
+
30
+ export function StatsBar({ total, p0, p1, p2, p3 }: StatsBarProps) {
31
+ return (
32
+ <div className="grid grid-cols-2 gap-3 xl:grid-cols-5">
33
+ <StatTile label="Total" value={total} accent="border-gray-800 bg-gray-900" tone="text-white" />
34
+ <StatTile label="P0 crítico" value={p0} accent="border-red-900/60 bg-red-950/40" tone="text-red-300" />
35
+ <StatTile label="P1 urgente" value={p1} accent="border-orange-900/60 bg-orange-950/40" tone="text-orange-300" />
36
+ <StatTile label="P2 moderado" value={p2} accent="border-yellow-900/60 bg-yellow-950/30" tone="text-yellow-200" />
37
+ <StatTile label="P3 bajo" value={p3} accent="border-emerald-900/60 bg-emerald-950/30" tone="text-emerald-300" />
38
+ </div>
39
+ );
40
+ }
frontend/components/UploadPanel.tsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { AlertCircle, FileText, Loader2, Play, SendHorizontal } from "lucide-react";
5
+ import { api } from "@/lib/api";
6
+ import type { CrisisRoomSummary } from "@/lib/types";
7
+
8
+ interface UploadPanelProps {
9
+ onResults: (summary: CrisisRoomSummary, demoMode: boolean) => void;
10
+ onProcessing: (processing: boolean) => void;
11
+ processing: boolean;
12
+ onError?: (message: string) => void;
13
+ }
14
+
15
+ const QUICK_TEST_MESSAGES = [
16
+ "Hay una familia atrapada en el techo de una casa en Barrio Santa Ana, el agua sigue subiendo.",
17
+ "Adulto mayor con posible fractura en el centro comunitario, necesita atención médica.",
18
+ "Tenemos 20 personas refugiadas en la escuela, falta agua potable.",
19
+ ];
20
+
21
+ export function UploadPanel({
22
+ onResults,
23
+ onProcessing,
24
+ processing,
25
+ onError,
26
+ }: UploadPanelProps) {
27
+ const [rawMessages, setRawMessages] = useState(QUICK_TEST_MESSAGES.join("\n"));
28
+ const messages = useMemo(
29
+ () => rawMessages.split("\n").map((line) => line.trim()).filter(Boolean),
30
+ [rawMessages],
31
+ );
32
+
33
+ async function handleLoadDemo() {
34
+ onProcessing(true);
35
+ try {
36
+ const res = await api.runDemo();
37
+ onResults(res.data as CrisisRoomSummary, false);
38
+ } catch {
39
+ onError?.("No se pudo ejecutar el escenario demo. Verifica que el backend esté disponible.");
40
+ } finally {
41
+ onProcessing(false);
42
+ }
43
+ }
44
+
45
+ async function handleQuickProcess() {
46
+ if (messages.length === 0) {
47
+ return;
48
+ }
49
+
50
+ const sessionId = crypto.randomUUID();
51
+ const payload = {
52
+ session_id: sessionId,
53
+ scenario_name: "Quick Manual Intake",
54
+ reports: messages.map((content) => ({
55
+ id: crypto.randomUUID(),
56
+ session_id: sessionId,
57
+ report_type: "text",
58
+ content,
59
+ metadata: { source: "frontend_quick_test" },
60
+ created_at: new Date().toISOString(),
61
+ })),
62
+ };
63
+
64
+ onProcessing(true);
65
+ try {
66
+ const res = await api.processBatch(payload);
67
+ onResults(res.data as CrisisRoomSummary, false);
68
+ } catch {
69
+ onError?.("No se pudo procesar la prueba rápida. Revisa la conexión con el backend.");
70
+ } finally {
71
+ onProcessing(false);
72
+ }
73
+ }
74
+
75
+ return (
76
+ <div className="space-y-4">
77
+ <div className="rounded-lg border border-gray-800 bg-gray-950/60 p-3">
78
+ <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
79
+ Demo recomendada
80
+ </p>
81
+ <p className="mt-1 text-sm leading-6 text-gray-300">
82
+ Ejecuta el escenario Santa Ana para mostrar la tubería completa: ingestión, normalización,
83
+ deduplicación, prioridad, recursos y despacho.
84
+ </p>
85
+ <button
86
+ onClick={handleLoadDemo}
87
+ disabled={processing}
88
+ className="mt-3 flex w-full items-center justify-center gap-2 rounded-lg bg-orange-600 px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-orange-500 disabled:cursor-not-allowed disabled:opacity-60"
89
+ >
90
+ {processing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
91
+ Ejecutar Demo Santa Ana
92
+ </button>
93
+ </div>
94
+
95
+ <div className="rounded-lg border border-gray-800 bg-gray-950/60 p-3">
96
+ <div className="flex items-start gap-2">
97
+ <FileText className="mt-0.5 h-4 w-4 text-blue-400" />
98
+ <div>
99
+ <p className="text-xs font-semibold uppercase tracking-wide text-gray-400">
100
+ Prueba rápida
101
+ </p>
102
+ <p className="mt-1 text-sm leading-6 text-gray-300">
103
+ Pega mensajes de texto separados por línea para validar el backend sin depender del
104
+ dataset demo.
105
+ </p>
106
+ </div>
107
+ </div>
108
+
109
+ <textarea
110
+ value={rawMessages}
111
+ onChange={(e) => setRawMessages(e.target.value)}
112
+ rows={8}
113
+ className="mt-3 w-full resize-none rounded-lg border border-gray-800 bg-gray-900 px-3 py-3 text-sm text-gray-100 outline-none ring-0 placeholder:text-gray-500 focus:border-blue-600"
114
+ placeholder="Un reporte por línea"
115
+ />
116
+
117
+ <div className="mt-3 flex items-center justify-between gap-3">
118
+ <div className="flex items-center gap-2 text-xs text-gray-400">
119
+ <AlertCircle className="h-3.5 w-3.5" />
120
+ <span>{messages.length} mensajes listos para procesar</span>
121
+ </div>
122
+ <button
123
+ onClick={handleQuickProcess}
124
+ disabled={processing || messages.length === 0}
125
+ className="flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-500 disabled:cursor-not-allowed disabled:opacity-60"
126
+ >
127
+ {processing ? <Loader2 className="h-4 w-4 animate-spin" /> : <SendHorizontal className="h-4 w-4" />}
128
+ Procesar texto
129
+ </button>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
frontend/dev.log ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ > frontend@0.1.0 dev
3
+ > next dev -p 3000
4
+
5
+ Error: spawn EPERM
6
+ at ChildProcess.spawn (node:internal/child_process:421:11)
7
+ at spawn (node:child_process:796:9)
8
+ at fork (node:child_process:174:10)
9
+ at C:\Users\Acer\Hackathon\ReliefLensAI\frontend\node_modules\next\dist\cli\next-dev.js:253:45
10
+ at new Promise (<anonymous>)
11
+ at startServer (C:\Users\Acer\Hackathon\ReliefLensAI\frontend\node_modules\next\dist\cli\next-dev.js:221:16)
12
+ at runDevServer (C:\Users\Acer\Hackathon\ReliefLensAI\frontend\node_modules\next\dist\cli\next-dev.js:355:23)
13
+ at Module.nextDev (C:\Users\Acer\Hackathon\ReliefLensAI\frontend\node_modules\next\dist\cli\next-dev.js:363:11) {
14
+ errno: -4048,
15
+ code: 'EPERM',
16
+ syscall: 'spawn'
17
+ }
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/lib/api.ts ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from "axios";
2
+
3
+ const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080";
4
+
5
+ export const api = {
6
+ getHealth: () => axios.get(`${BASE_URL}/health`),
7
+ runDemo: () => axios.post(`${BASE_URL}/api/demo/run`),
8
+ getDemoScenario: () => axios.get(`${BASE_URL}/api/demo/scenario`),
9
+ processBatch: (payload: unknown) => axios.post(`${BASE_URL}/api/reports/process`, payload),
10
+ getIncidents: (sessionId?: string) =>
11
+ axios.get(`${BASE_URL}/api/incidents`, {
12
+ params: sessionId ? { session_id: sessionId } : {},
13
+ }),
14
+ getIncident: (id: string) => axios.get(`${BASE_URL}/api/incidents/${id}`),
15
+ approveIncident: (id: string) =>
16
+ axios.patch(`${BASE_URL}/api/incidents/${id}`, { human_approved: true }),
17
+ generateDispatch: (id: string) =>
18
+ axios.post(`${BASE_URL}/api/incidents/${id}/dispatch-message`),
19
+ getAMDMetrics: () => axios.get(`${BASE_URL}/api/amd/performance`),
20
+ getCrisisRoom: (sessionId: string) =>
21
+ axios.get(`${BASE_URL}/api/crisis-room/${sessionId}`),
22
+ };
frontend/lib/types.ts ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Priority = "P0" | "P1" | "P2" | "P3";
2
+
3
+ export type IncidentStatus = "new" | "acknowledged" | "in_progress" | "resolved";
4
+
5
+ export interface Coordinates {
6
+ lat: number;
7
+ lon: number;
8
+ }
9
+
10
+ export interface EvidenceItem {
11
+ id: string;
12
+ report_id?: string;
13
+ modality: "text" | "image" | "audio" | "csv" | "location" | "video";
14
+ description: string;
15
+ file_path?: string | null;
16
+ timestamp?: string;
17
+ }
18
+
19
+ export interface ResourceRecommendation {
20
+ id: string;
21
+ incident_id: string;
22
+ resource_type: string;
23
+ description: string;
24
+ quantity?: number | null;
25
+ urgency: string;
26
+ rationale: string;
27
+ }
28
+
29
+ export interface DispatchMessage {
30
+ id: string;
31
+ incident_id: string;
32
+ channel: string;
33
+ message: string;
34
+ brigade_target?: string | null;
35
+ created_at?: string;
36
+ approved?: boolean;
37
+ }
38
+
39
+ export interface Incident {
40
+ id: string;
41
+ session_id?: string;
42
+ title: string;
43
+ description: string;
44
+ priority: Priority;
45
+ status: IncidentStatus;
46
+ signal_ids?: string[];
47
+ evidence: EvidenceItem[];
48
+ location?: string | null;
49
+ coordinates?: Coordinates | null;
50
+ affected_people?: number | null;
51
+ confidence: number;
52
+ created_at: string;
53
+ updated_at?: string;
54
+ human_approved: boolean;
55
+ notes?: string | null;
56
+ }
57
+
58
+ export interface AMDPerformanceMetric {
59
+ timestamp?: string;
60
+ gpu_utilization: number;
61
+ memory_used_gb: number;
62
+ memory_total_gb: number;
63
+ tokens_per_second: number;
64
+ requests_processed: number;
65
+ avg_latency_ms: number;
66
+ model_name: string;
67
+ rocm_version?: string | null;
68
+ power_watts?: number | null;
69
+ }
70
+
71
+ export interface CrisisRoomSummary {
72
+ session_id: string;
73
+ scenario_name: string;
74
+ total_reports: number;
75
+ total_signals: number;
76
+ total_incidents: number;
77
+ incidents_by_priority: Record<string, number>;
78
+ critical_incidents: Incident[];
79
+ resource_recommendations: ResourceRecommendation[];
80
+ dispatch_messages: DispatchMessage[];
81
+ amd_metrics?: AMDPerformanceMetric | null;
82
+ processing_time_seconds: number;
83
+ created_at: string;
84
+ status: string;
85
+ }
frontend/next.config.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+ import path from "node:path";
3
+
4
+ const nextConfig: NextConfig = {
5
+ turbopack: {
6
+ root: path.join(__dirname),
7
+ },
8
+ };
9
+
10
+ export default nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev -p 3000",
7
+ "build": "next build",
8
+ "start": "next start -p 3000",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@tanstack/react-query": "^5.100.9",
13
+ "axios": "^1.16.0",
14
+ "lucide-react": "^1.14.0",
15
+ "next": "16.2.4",
16
+ "react": "19.2.4",
17
+ "react-dom": "19.2.4",
18
+ "recharts": "^3.8.1"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/postcss": "^4",
22
+ "@types/node": "^20",
23
+ "@types/react": "^19",
24
+ "@types/react-dom": "^19",
25
+ "eslint": "^9",
26
+ "eslint-config-next": "16.2.4",
27
+ "tailwindcss": "^4",
28
+ "typescript": "^5"
29
+ }
30
+ }
frontend/postcss.config.mjs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ const config = {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
6
+
7
+ export default config;
frontend/public/file.svg ADDED
frontend/public/globe.svg ADDED
frontend/public/next.svg ADDED
frontend/public/vercel.svg ADDED
frontend/public/window.svg ADDED
frontend/start.log ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ > frontend@0.1.0 start
3
+ > next start -p 3000
4
+
5
+ ▲ Next.js 16.2.4
6
+ - Local: http://localhost:3000
7
+ - Network: http://172.23.64.1:3000
8
+ ✓ Ready in 533ms
9
+ ⚠ Warning: Next.js inferred your workspace root, but it may not be correct.
10
+ We detected multiple lockfiles and selected the directory of C:\Users\Acer\package-lock.json as the root directory.
11
+ To silence this warning, set `outputFileTracingRoot` in your Next.js config, or consider removing one of the lockfiles if it's not needed.
12
+ See https://nextjs.org/docs/app/api-reference/config/next-config-js/output#caveats for more information.
13
+ Detected additional lockfiles:
14
+ * C:\Users\Acer\Hackathon\ReliefLensAI\frontend\package-lock.json
15
+
frontend/tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }