Deploy Next.js frontend Docker Space
Browse files- Dockerfile +27 -0
- README.md +16 -6
- frontend/.gitignore +41 -0
- frontend/AGENTS.md +5 -0
- frontend/CLAUDE.md +1 -0
- frontend/README.md +36 -0
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +29 -0
- frontend/app/layout.tsx +22 -0
- frontend/app/page.tsx +300 -0
- frontend/components/AMDPanel.tsx +106 -0
- frontend/components/IncidentCard.tsx +204 -0
- frontend/components/PriorityBadge.tsx +18 -0
- frontend/components/StatsBar.tsx +40 -0
- frontend/components/UploadPanel.tsx +134 -0
- frontend/dev.log +17 -0
- frontend/eslint.config.mjs +18 -0
- frontend/lib/api.ts +22 -0
- frontend/lib/types.ts +85 -0
- frontend/next.config.ts +10 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +30 -0
- frontend/postcss.config.mjs +7 -0
- frontend/public/file.svg +1 -0
- frontend/public/globe.svg +1 -0
- frontend/public/next.svg +1 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/start.log +15 -0
- frontend/tsconfig.json +34 -0
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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|