SCGR commited on
Commit
a406193
Β·
1 Parent(s): 6138deb
frontend/src/components/HeaderNav.tsx CHANGED
@@ -9,6 +9,12 @@ import {
9
  SettingsIcon,
10
  } from "@ifrc-go/icons";
11
 
 
 
 
 
 
 
12
  const navItems = [
13
  { to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
14
  { to: "/explore", label: "Explore", Icon: SearchLineIcon },
@@ -30,8 +36,8 @@ export default function HeaderNav() {
30
  className="flex items-center gap-4 min-w-0 cursor-pointer group transition-all duration-200 hover:scale-105"
31
  onClick={() => {
32
  if (location.pathname === "/upload") {
33
- if ((window as any).confirmNavigationIfNeeded) {
34
- (window as any).confirmNavigationIfNeeded('/');
35
  return;
36
  }
37
  if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
@@ -68,8 +74,8 @@ export default function HeaderNav() {
68
  }`}
69
  onClick={() => {
70
  if (location.pathname === "/upload") {
71
- if ((window as any).confirmNavigationIfNeeded) {
72
- (window as any).confirmNavigationIfNeeded(to);
73
  return;
74
  }
75
  if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
@@ -104,8 +110,8 @@ export default function HeaderNav() {
104
  }`}
105
  onClick={() => {
106
  if (location.pathname === "/upload") {
107
- if ((window as any).confirmNavigationIfNeeded) {
108
- (window as any).confirmNavigationIfNeeded('/help');
109
  return;
110
  }
111
  if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
 
9
  SettingsIcon,
10
  } from "@ifrc-go/icons";
11
 
12
+ declare global {
13
+ interface Window {
14
+ confirmNavigationIfNeeded?: (to: string) => void;
15
+ }
16
+ }
17
+
18
  const navItems = [
19
  { to: "/upload", label: "Upload", Icon: UploadCloudLineIcon },
20
  { to: "/explore", label: "Explore", Icon: SearchLineIcon },
 
36
  className="flex items-center gap-4 min-w-0 cursor-pointer group transition-all duration-200 hover:scale-105"
37
  onClick={() => {
38
  if (location.pathname === "/upload") {
39
+ if (window.confirmNavigationIfNeeded) {
40
+ window.confirmNavigationIfNeeded('/');
41
  return;
42
  }
43
  if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
 
74
  }`}
75
  onClick={() => {
76
  if (location.pathname === "/upload") {
77
+ if (window.confirmNavigationIfNeeded) {
78
+ window.confirmNavigationIfNeeded(to);
79
  return;
80
  }
81
  if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
 
110
  }`}
111
  onClick={() => {
112
  if (location.pathname === "/upload") {
113
+ if (window.confirmNavigationIfNeeded) {
114
+ window.confirmNavigationIfNeeded('/help');
115
  return;
116
  }
117
  if (!confirm("You have unsaved changes. Are you sure you want to leave?")) {
frontend/src/pages/AnalyticsPage/AnalyticsPage.tsx CHANGED
@@ -13,7 +13,7 @@ import {
13
  createNumberColumn,
14
  numericIdSelector
15
  } from '@ifrc-go/ui/utils';
16
- import { useState, useEffect, useMemo } from 'react';
17
  import styles from './AnalyticsPage.module.css';
18
 
19
  interface AnalyticsData {
@@ -70,6 +70,18 @@ interface ModelData {
70
  totalScore: number;
71
  }
72
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  export default function AnalyticsPage() {
74
  const [data, setData] = useState<AnalyticsData | null>(null);
75
  const [loading, setLoading] = useState(true);
@@ -83,12 +95,7 @@ export default function AnalyticsPage() {
83
  { key: 'vlm' as const, label: 'VLM Analytics' }
84
  ];
85
 
86
- useEffect(() => {
87
- fetchAnalytics();
88
- fetchLookupData();
89
- }, []);
90
-
91
- async function fetchAnalytics() {
92
  setLoading(true);
93
  try {
94
  const res = await fetch('/api/images/');
@@ -102,11 +109,11 @@ export default function AnalyticsPage() {
102
  models: {},
103
  };
104
 
105
- maps.forEach((map: any) => {
106
  if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
107
  if (map.event_type) analytics.types[map.event_type] = (analytics.types[map.event_type] || 0) + 1;
108
  if (map.countries) {
109
- map.countries.forEach((c: any) => {
110
  if (c.r_code) analytics.regions[c.r_code] = (analytics.regions[c.r_code] || 0) + 1;
111
  });
112
  }
@@ -155,15 +162,15 @@ export default function AnalyticsPage() {
155
  });
156
 
157
  setData(analytics);
158
- } catch (e) {
159
 
160
  setData(null);
161
  } finally {
162
  setLoading(false);
163
  }
164
- }
165
 
166
- async function fetchLookupData() {
167
  try {
168
  const [sourcesRes, typesRes, regionsRes] = await Promise.all([
169
  fetch('/api/sources'),
@@ -176,20 +183,25 @@ export default function AnalyticsPage() {
176
  setSourcesLookup(sources);
177
  setTypesLookup(types);
178
  setRegionsLookup(regions);
179
- } catch (e) {
180
 
181
  }
182
- }
 
 
 
 
 
183
 
184
- const getSourceLabel = (code: string) => {
185
  const source = sourcesLookup.find(s => s.s_code === code);
186
  return source ? source.label : code;
187
- };
188
 
189
- const getTypeLabel = (code: string) => {
190
  const type = typesLookup.find(t => t.t_code === code);
191
  return type ? type.label : code;
192
- };
193
 
194
  const regionsTableData = useMemo(() => {
195
  if (!data || !regionsLookup.length) return [];
 
13
  createNumberColumn,
14
  numericIdSelector
15
  } from '@ifrc-go/ui/utils';
16
+ import { useState, useEffect, useMemo, useCallback } from 'react';
17
  import styles from './AnalyticsPage.module.css';
18
 
19
  interface AnalyticsData {
 
70
  totalScore: number;
71
  }
72
 
73
+ interface MapData {
74
+ source?: string;
75
+ event_type?: string;
76
+ countries?: Array<{ r_code?: string }>;
77
+ captions?: Array<{
78
+ model?: string;
79
+ accuracy?: number;
80
+ context?: number;
81
+ usability?: number;
82
+ }>;
83
+ }
84
+
85
  export default function AnalyticsPage() {
86
  const [data, setData] = useState<AnalyticsData | null>(null);
87
  const [loading, setLoading] = useState(true);
 
95
  { key: 'vlm' as const, label: 'VLM Analytics' }
96
  ];
97
 
98
+ const fetchAnalytics = useCallback(async () => {
 
 
 
 
 
99
  setLoading(true);
100
  try {
101
  const res = await fetch('/api/images/');
 
109
  models: {},
110
  };
111
 
112
+ maps.forEach((map: MapData) => {
113
  if (map.source) analytics.sources[map.source] = (analytics.sources[map.source] || 0) + 1;
114
  if (map.event_type) analytics.types[map.event_type] = (analytics.types[map.event_type] || 0) + 1;
115
  if (map.countries) {
116
+ map.countries.forEach((c) => {
117
  if (c.r_code) analytics.regions[c.r_code] = (analytics.regions[c.r_code] || 0) + 1;
118
  });
119
  }
 
162
  });
163
 
164
  setData(analytics);
165
+ } catch {
166
 
167
  setData(null);
168
  } finally {
169
  setLoading(false);
170
  }
171
+ }, [sourcesLookup, typesLookup, regionsLookup]);
172
 
173
+ const fetchLookupData = useCallback(async () => {
174
  try {
175
  const [sourcesRes, typesRes, regionsRes] = await Promise.all([
176
  fetch('/api/sources'),
 
183
  setSourcesLookup(sources);
184
  setTypesLookup(types);
185
  setRegionsLookup(regions);
186
+ } catch {
187
 
188
  }
189
+ }, []);
190
+
191
+ useEffect(() => {
192
+ fetchAnalytics();
193
+ fetchLookupData();
194
+ }, [fetchAnalytics, fetchLookupData]);
195
 
196
+ const getSourceLabel = useCallback((code: string) => {
197
  const source = sourcesLookup.find(s => s.s_code === code);
198
  return source ? source.label : code;
199
+ }, [sourcesLookup]);
200
 
201
+ const getTypeLabel = useCallback((code: string) => {
202
  const type = typesLookup.find(t => t.t_code === code);
203
  return type ? type.label : code;
204
+ }, [typesLookup]);
205
 
206
  const regionsTableData = useMemo(() => {
207
  if (!data || !regionsLookup.length) return [];
frontend/src/pages/MapDetailsPage/MapDetailPage.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { PageContainer, Container, Button, Spinner, SegmentInput, TextInput, SelectInput, MultiSelectInput } from '@ifrc-go/ui';
2
  import { useParams, useNavigate } from 'react-router-dom';
3
- import { useState, useEffect, useMemo } from 'react';
4
  import { ChevronLeftLineIcon, ChevronRightLineIcon } from '@ifrc-go/icons';
5
  import styles from './MapDetailPage.module.css';
6
 
@@ -63,17 +63,7 @@ export default function MapDetailPage() {
63
  { key: 'mapDetails' as const, label: 'Map Details' }
64
  ];
65
 
66
- useEffect(() => {
67
- if (!mapId) {
68
- setError('Map ID is required');
69
- setLoading(false);
70
- return;
71
- }
72
-
73
- fetchMapData(mapId);
74
- }, [mapId]);
75
-
76
- const fetchMapData = async (id: string) => {
77
  setIsNavigating(true);
78
  setLoading(true);
79
 
@@ -93,7 +83,17 @@ export default function MapDetailPage() {
93
  setLoading(false);
94
  setIsNavigating(false);
95
  }
96
- };
 
 
 
 
 
 
 
 
 
 
97
 
98
  const checkNavigationAvailability = async (currentId: string) => {
99
  try {
 
1
  import { PageContainer, Container, Button, Spinner, SegmentInput, TextInput, SelectInput, MultiSelectInput } from '@ifrc-go/ui';
2
  import { useParams, useNavigate } from 'react-router-dom';
3
+ import { useState, useEffect, useMemo, useCallback } from 'react';
4
  import { ChevronLeftLineIcon, ChevronRightLineIcon } from '@ifrc-go/icons';
5
  import styles from './MapDetailPage.module.css';
6
 
 
63
  { key: 'mapDetails' as const, label: 'Map Details' }
64
  ];
65
 
66
+ const fetchMapData = useCallback(async (id: string) => {
 
 
 
 
 
 
 
 
 
 
67
  setIsNavigating(true);
68
  setLoading(true);
69
 
 
83
  setLoading(false);
84
  setIsNavigating(false);
85
  }
86
+ }, []);
87
+
88
+ useEffect(() => {
89
+ if (!mapId) {
90
+ setError('Map ID is required');
91
+ setLoading(false);
92
+ return;
93
+ }
94
+
95
+ fetchMapData(mapId);
96
+ }, [mapId, fetchMapData]);
97
 
98
  const checkNavigationAvailability = async (currentId: string) => {
99
  try {
frontend/src/pages/UploadPage/UploadPage.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from 'react';
2
  import type { DragEvent } from 'react';
3
  import {
4
  PageContainer, Heading, Button,
@@ -80,7 +80,7 @@ export default function UploadPage() {
80
 
81
 
82
 
83
- const handleNavigation = (to: string) => {
84
  if (uploadedImageIdRef.current) {
85
  if (confirm("Leave page? Your uploaded image will be deleted.")) {
86
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" })
@@ -92,17 +92,17 @@ export default function UploadPage() {
92
  } else {
93
  navigate(to);
94
  }
95
- };
96
 
97
  useEffect(() => {
98
- (window as any).confirmNavigationIfNeeded = (to: string) => {
99
  handleNavigation(to);
100
  };
101
 
102
  return () => {
103
- delete (window as any).confirmNavigationIfNeeded;
104
  };
105
- }, []);
106
 
107
  useEffect(() => {
108
  const handleBeforeUnload = (event: BeforeUnloadEvent) => {
 
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
  import type { DragEvent } from 'react';
3
  import {
4
  PageContainer, Heading, Button,
 
80
 
81
 
82
 
83
+ const handleNavigation = useCallback((to: string) => {
84
  if (uploadedImageIdRef.current) {
85
  if (confirm("Leave page? Your uploaded image will be deleted.")) {
86
  fetch(`/api/images/${uploadedImageIdRef.current}`, { method: "DELETE" })
 
92
  } else {
93
  navigate(to);
94
  }
95
+ }, [navigate]);
96
 
97
  useEffect(() => {
98
+ window.confirmNavigationIfNeeded = (to: string) => {
99
  handleNavigation(to);
100
  };
101
 
102
  return () => {
103
+ delete window.confirmNavigationIfNeeded;
104
  };
105
+ }, [handleNavigation]);
106
 
107
  useEffect(() => {
108
  const handleBeforeUnload = (event: BeforeUnloadEvent) => {
py_backend/app/main.py CHANGED
@@ -2,7 +2,7 @@ import os
2
  from fastapi import FastAPI, HTTPException
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from fastapi.staticfiles import StaticFiles
5
- from fastapi.responses import FileResponse, JSONResponse
6
 
7
  from app.config import settings
8
  from app.routers import upload, caption, metadata, models
@@ -10,43 +10,53 @@ from app.routers.images import router as images_router
10
 
11
  app = FastAPI(title="PromptAid Vision")
12
 
13
- # CORS: allow localhost dev and all *.hf.space
14
  app.add_middleware(
15
  CORSMiddleware,
16
  allow_origins=[
17
  "http://localhost:3000",
18
  "http://localhost:5173",
19
  ],
20
- allow_origin_regex=r"https://.*\.hf\.space$", # Hugging Face subdomains
21
- allow_credentials=False, # must be False if using "*" or regex
22
  allow_methods=["*"],
23
  allow_headers=["*"],
24
  )
25
 
26
- # ---- API routers (keep them under /api) ----
27
- app.include_router(caption.router, prefix="/api", tags=["captions"])
28
- app.include_router(metadata.router, prefix="/api", tags=["metadata"])
29
- app.include_router(models.router, prefix="/api", tags=["models"])
30
- app.include_router(upload.router, prefix="/api/images", tags=["images"])
31
- app.include_router(images_router, prefix="/api/contribute", tags=["contribute"])
32
 
33
- # Simple health endpoint for HF health checks
34
  @app.get("/health", include_in_schema=False, response_class=JSONResponse)
35
  async def health():
36
  return {"status": "ok"}
37
 
38
- # ---- Serve built frontend (Vite) ----
 
 
 
 
 
 
 
 
39
  STATIC_DIR = os.path.join(os.path.dirname(__file__), "..", "static")
40
- app.mount("/", StaticFiles(directory=STATIC_DIR, html=True), name="static")
 
41
 
42
- # SPA fallback for client-side routes; don't intercept API or docs
43
- @app.get("/{full_path:path}", include_in_schema=False)
44
  def spa_fallback(full_path: str):
45
- if full_path.startswith(("api", "docs", "redoc", "openapi")):
46
- raise HTTPException(status_code=404, detail="Not Found")
47
- return FileResponse(os.path.join(STATIC_DIR, "index.html"))
 
48
 
49
  print("πŸš€ PromptAid Vision API server ready")
50
- print("πŸ“Š Available endpoints: /api/images, /api/captions, /api/metadata, /api/models")
51
  print(f"🌍 Environment: {settings.ENVIRONMENT}")
52
- print("πŸ”— CORS enabled for localhost and *.hf.space")
 
2
  from fastapi import FastAPI, HTTPException
3
  from fastapi.middleware.cors import CORSMiddleware
4
  from fastapi.staticfiles import StaticFiles
5
+ from fastapi.responses import FileResponse, JSONResponse, HTMLResponse
6
 
7
  from app.config import settings
8
  from app.routers import upload, caption, metadata, models
 
10
 
11
  app = FastAPI(title="PromptAid Vision")
12
 
13
+ # CORS: localhost dev + *.hf.space
14
  app.add_middleware(
15
  CORSMiddleware,
16
  allow_origins=[
17
  "http://localhost:3000",
18
  "http://localhost:5173",
19
  ],
20
+ allow_origin_regex=r"https://.*\.hf\.space$",
21
+ allow_credentials=False,
22
  allow_methods=["*"],
23
  allow_headers=["*"],
24
  )
25
 
26
+ # API routers
27
+ app.include_router(caption.router, prefix="/api", tags=["captions"])
28
+ app.include_router(metadata.router, prefix="/api", tags=["metadata"])
29
+ app.include_router(models.router, prefix="/api", tags=["models"])
30
+ app.include_router(upload.router, prefix="/api/images", tags=["images"])
31
+ app.include_router(images_router, prefix="/api/contribute", tags=["contribute"])
32
 
33
+ # Health & simple root
34
  @app.get("/health", include_in_schema=False, response_class=JSONResponse)
35
  async def health():
36
  return {"status": "ok"}
37
 
38
+ @app.get("/", include_in_schema=False, response_class=HTMLResponse)
39
+ def root():
40
+ return """<!doctype html>
41
+ <title>PromptAid Vision</title>
42
+ <h1>PromptAid Vision</h1>
43
+ <p>OK</p>
44
+ <p><a href="/app/">Open UI</a> β€’ <a href="/docs">API Docs</a></p>"""
45
+
46
+ # Serve built frontend under /app (expects files in py_backend/static)
47
  STATIC_DIR = os.path.join(os.path.dirname(__file__), "..", "static")
48
+ if os.path.isdir(STATIC_DIR):
49
+ app.mount("/app", StaticFiles(directory=STATIC_DIR, html=True), name="static")
50
 
51
+ # SPA fallback only for /app/* routes
52
+ @app.get("/app/{full_path:path}", include_in_schema=False)
53
  def spa_fallback(full_path: str):
54
+ index = os.path.join(STATIC_DIR, "index.html")
55
+ if os.path.isfile(index):
56
+ return FileResponse(index)
57
+ raise HTTPException(status_code=404, detail="Not Found")
58
 
59
  print("πŸš€ PromptAid Vision API server ready")
60
+ print("πŸ“Š Endpoints: /api/images, /api/captions, /api/metadata, /api/models")
61
  print(f"🌍 Environment: {settings.ENVIRONMENT}")
62
+ print("πŸ”— CORS: localhost + *.hf.space")