Initial GeoIntel V1 foundation
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
dist
|
||||
__pycache__
|
||||
*.pyc
|
||||
.pytest_cache
|
||||
.vite
|
||||
.env
|
||||
@@ -0,0 +1,16 @@
|
||||
FROM node:20-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27-alpine AS runtime
|
||||
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,189 @@
|
||||
# GeoIntel Frontend (Sprint 4)
|
||||
|
||||
React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
||||
|
||||
## Scope implemented
|
||||
- API client layer (`src/services/api`)
|
||||
- Project and area list/create flows
|
||||
- Vector and raster dataset upload + metadata display
|
||||
- MapLibre map with uploaded feature preview
|
||||
|
||||
## Sprint 2 additions
|
||||
- Dataset manager details now shows:
|
||||
- dataset type
|
||||
- processing status
|
||||
- file size
|
||||
- vector feature count
|
||||
- vector bounds
|
||||
- file metadata (original/stored filename, MIME type, SHA256)
|
||||
- raster metadata preview when available
|
||||
- Vector inspection and raster metadata endpoint support in API client
|
||||
- Readiness indicator for uploaded datasets
|
||||
|
||||
## Sprint 3 additions
|
||||
- Dataset detail panel now includes:
|
||||
- available operations
|
||||
- operation run actions (`clip`, `buffer`, `intersect`)
|
||||
- linked job list and status details
|
||||
- derived dataset navigation from job output
|
||||
- Raster operation callouts for unavailable processing states
|
||||
- Vector operation summaries integrated in selected dataset view
|
||||
|
||||
## Sprint 4 additions
|
||||
- Dataset detail panel now includes raster-specific runtime metadata:
|
||||
- driver, dimensions, band count, bounds, CRS
|
||||
- storage details (`size_bytes`, `checksum_sha256`)
|
||||
- Added action buttons for raster operations:
|
||||
- regenerate/inspect metadata
|
||||
- generate preview
|
||||
- generate tiles
|
||||
- clip by selected area
|
||||
- Added clear unavailable states for raster ops when backend returns `RASTER_PROCESSING_UNAVAILABLE`
|
||||
- Added operation/job result visibility for raster runs with derived dataset navigation
|
||||
|
||||
## Sprint 5 additions
|
||||
- Added raster band statistics display in the dataset detail panel (min/max/mean/std/nodata ratio/valid pixel count).
|
||||
- Added raster reproject workflow in UI (target CRS + resampling) with visible errors for invalid CRS/dependency gaps.
|
||||
- Added stronger raster tile/clip result context with consistent job status display and derived output links where produced.
|
||||
- Added tile manifest-aware controls for raster tile generation parameters.
|
||||
|
||||
## Sprint 6 additions
|
||||
- Added spectral index controls in dataset detail panel:
|
||||
- NDVI with NIR/Red band inputs
|
||||
- NDWI with NIR/Green band inputs
|
||||
- NDBI with SWIR/NIR band inputs
|
||||
- Added job-driven execution for local spectral index operations and result dataset linking.
|
||||
- Added clear error surfacing for dependency-unavailable index execution (`RASTER_PROCESSING_UNAVAILABLE`).
|
||||
- Added CRS/bounds/resolution context visibility for raster index source inspection.
|
||||
|
||||
## Sprint 7B additions
|
||||
- Added a lightweight Provider Capabilities panel.
|
||||
- The panel lists GRB, OSM, manual and fixture provider status, configured state, authority level, supported layers, supported geometry types, query modes and limitation messages.
|
||||
- GRB and OSM are shown as `not_configured`; the UI does not expose a live import/download action for them.
|
||||
- Existing dataset, reference and QA/QC UI remains unchanged.
|
||||
|
||||
## Sprint 8 additions
|
||||
- Added a minimal Detection Lab panel.
|
||||
- The panel lists detection model capabilities and clearly shows configured/not_configured status.
|
||||
- Users can select a raster dataset, choose a confidence threshold and request a detection run.
|
||||
- Unavailable model responses are shown honestly with the backend error code/message.
|
||||
- The UI does not claim real YOLO/PyTorch inference is enabled.
|
||||
|
||||
## Sprint 8B additions
|
||||
- Detection Lab now exposes the `yolo-configured` capability reported by the backend.
|
||||
- When `yolo-configured` is selected, users can provide an existing raster tile manifest path.
|
||||
- The UI still does not download models or create fake detections; backend status and error codes remain the source of truth.
|
||||
|
||||
## Sprint 8C additions
|
||||
- Detection Lab now lists detection analysis runs and persisted detections.
|
||||
- Users can filter detections by class and minimum confidence.
|
||||
- Selected detection GeoJSON is rendered on the existing MapLibre workbench map.
|
||||
- Detection QA compares a selected detection run against a reference dataset and displays persisted QA metrics.
|
||||
- No segmentation UI is introduced in Sprint 8C.
|
||||
|
||||
## Sprint 9 additions
|
||||
- Added a minimal Segmentation Lab panel.
|
||||
- The panel lists segmentation model capabilities and clearly distinguishes placeholders, fixture/demo mode, SAM placeholder and YOLO-seg placeholder states.
|
||||
- Users can select a raster dataset, select a configured segmentation model, list segmentation runs and load persisted segmentation results.
|
||||
- Segmentation results display class, confidence, area, model, tile and mask path fields.
|
||||
- Selected segmentation GeoJSON is rendered through the existing MapLibre workbench map.
|
||||
- Segmentation QA compares a selected segmentation run against a reference dataset and displays persisted QA metrics.
|
||||
- Real SAM/YOLO-seg inference, model downloads and new AI dependencies are not introduced in Sprint 9.
|
||||
|
||||
## Sprint 10 maintainability updates
|
||||
- Split large workbench sections out of `src/App.tsx` without changing UI behavior:
|
||||
- `src/components/project/ProjectPanel.tsx`
|
||||
- `src/components/project/AreaPanel.tsx`
|
||||
- `src/components/providers/ProviderPanel.tsx`
|
||||
- `src/components/detection/DetectionLab.tsx`
|
||||
- `src/components/segmentation/SegmentationLab.tsx`
|
||||
- `App.tsx` still owns shared state orchestration and API calls; extracted components receive the same state and callbacks as props.
|
||||
- Existing MapLibre overlay behavior, dataset/reference flows, Detection Lab flows and Segmentation Lab flows are unchanged.
|
||||
|
||||
## Sprint 15 additions
|
||||
|
||||
- Added a Projects panel action to load the explicit offline demo workflow.
|
||||
- The action calls `POST /api/v1/demo/workflow` and refreshes projects, areas, datasets and run lists.
|
||||
- Demo data is labelled fixture/demo data and does not represent live GRB/OSM data or AI inference.
|
||||
|
||||
## Sprint 16 additions
|
||||
|
||||
- Added a QA/QC Results panel that lists persisted project quality checks and metric rows.
|
||||
- The panel calls `GET /api/v1/projects/{project_id}/quality-checks`.
|
||||
- Demo workflow loading and QA actions refresh the persisted QA/QC result list.
|
||||
|
||||
## Sprint 17 additions
|
||||
|
||||
- Added an Export Center panel.
|
||||
- The panel can create persisted exports for:
|
||||
- project metadata JSON
|
||||
- project report HTML
|
||||
- selected vector dataset GeoJSON
|
||||
- selected detection run GeoJSON
|
||||
- selected segmentation run GeoJSON
|
||||
- Export records are listed from `GET /api/v1/exports/projects/{project_id}/exports`.
|
||||
- JSON artifact preview uses `GET /api/v1/exports/{export_id}/content`.
|
||||
- Artifact downloads use `GET /api/v1/exports/{export_id}/download`.
|
||||
- The UI does not introduce live provider downloads, a report designer or new AI behavior.
|
||||
- The HTML report is a lightweight artifact built from persisted project, dataset, QA/QC summary and export history state; it is not a PDF/report designer.
|
||||
|
||||
## Release hardening updates
|
||||
|
||||
- Production builds split application code, React vendor code and MapLibre vendor code into separate chunks.
|
||||
- The MapLibre chunk is intentionally larger than generic app chunks because it contains the GIS map runtime; the Vite warning threshold is set to keep this known vendor dependency visible without warning on every release build.
|
||||
|
||||
## Raster dependency visibility
|
||||
|
||||
Raster metadata and raster ops may remain unavailable when backend raster stack is missing. In that case:
|
||||
|
||||
- raster uploads are still stored and listed
|
||||
- status becomes `failed`
|
||||
- backend returns explicit `RASTER_PROCESSING_UNAVAILABLE` responses for metadata/preview/clip/tile
|
||||
|
||||
## Run locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
|
||||
### Install dependencies
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run locally
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
### Type check and build
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Dockerized frontend
|
||||
|
||||
```bash
|
||||
docker compose up --build frontend
|
||||
```
|
||||
|
||||
When using the repository Docker Compose stack, the frontend is published on host port `1202`: `http://localhost:1202`.
|
||||
|
||||
The frontend API client uses same-origin requests by default. In Docker Compose, nginx serves the built frontend and reverse proxies `/api` and `/health` to the backend service, so browser clients on LAN hosts do not call their own `localhost:8000`.
|
||||
|
||||
## Useful repository scripts
|
||||
|
||||
- `bash scripts/frontend_install.sh`
|
||||
- `bash scripts/frontend_typecheck.sh`
|
||||
- `bash scripts/frontend_build.sh`
|
||||
- `bash scripts/frontend_dev.sh`
|
||||
|
||||
## Key docs
|
||||
- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
|
||||
- `docs/API_CONTRACTS.md`
|
||||
- `docs/REPOSITORY_CONVENTIONS.md`
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GeoIntel Kempen</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8000/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /health {
|
||||
proxy_pass http://backend:8000/health;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Generated
+2109
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "geointel-frontend",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"maplibre-gl": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.0",
|
||||
"@types/react-dom": "^18.2.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"typescript": "^5.5.4",
|
||||
"vite": "^5.4.1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,116 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
|
||||
interface GeoMapProps {
|
||||
data: GeoJSON.FeatureCollection | null
|
||||
}
|
||||
|
||||
function collectCoordinates(featureCollection: GeoJSON.FeatureCollection): maplibregl.LngLatBoundsLike | null {
|
||||
const coordinates: [number, number][] = []
|
||||
const walk = (coords: unknown) => {
|
||||
if (!Array.isArray(coords)) {
|
||||
return
|
||||
}
|
||||
if (coords.length === 2 && typeof coords[0] === 'number' && typeof coords[1] === 'number') {
|
||||
coordinates.push([coords[0], coords[1]])
|
||||
return
|
||||
}
|
||||
for (const item of coords) {
|
||||
walk(item)
|
||||
}
|
||||
}
|
||||
|
||||
for (const feature of featureCollection.features) {
|
||||
const geometry = feature.geometry as any
|
||||
if (geometry && geometry.coordinates) {
|
||||
walk(geometry.coordinates)
|
||||
}
|
||||
}
|
||||
|
||||
if (coordinates.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const xs = coordinates.map((point) => point[0])
|
||||
const ys = coordinates.map((point) => point[1])
|
||||
return [
|
||||
[Math.min(...xs), Math.min(...ys)],
|
||||
[Math.max(...xs), Math.max(...ys)],
|
||||
]
|
||||
}
|
||||
|
||||
function GeoMap({ data }: GeoMapProps): JSX.Element {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const mapRef = useRef<maplibregl.Map | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || mapRef.current) {
|
||||
return
|
||||
}
|
||||
|
||||
const map = new maplibregl.Map({
|
||||
container: containerRef.current,
|
||||
style: import.meta.env.VITE_MAP_STYLE_URL || 'https://demotiles.maplibre.org/style.json',
|
||||
center: [5.3, 51.3],
|
||||
zoom: 9,
|
||||
})
|
||||
map.addControl(new maplibregl.NavigationControl(), 'top-right')
|
||||
mapRef.current = map
|
||||
|
||||
return () => {
|
||||
map.remove()
|
||||
mapRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const map = mapRef.current
|
||||
if (!map) {
|
||||
return
|
||||
}
|
||||
|
||||
if (map.getSource('dataset')) {
|
||||
if (data) {
|
||||
;(map.getSource('dataset') as maplibregl.GeoJSONSource).setData(data)
|
||||
} else {
|
||||
if (map.getLayer('dataset-fill')) {
|
||||
map.removeLayer('dataset-fill')
|
||||
}
|
||||
if (map.getLayer('dataset-line')) {
|
||||
map.removeLayer('dataset-line')
|
||||
}
|
||||
map.removeSource('dataset')
|
||||
return
|
||||
}
|
||||
} else if (data) {
|
||||
map.addSource('dataset', { type: 'geojson', data })
|
||||
map.addLayer({
|
||||
id: 'dataset-fill',
|
||||
type: 'fill',
|
||||
source: 'dataset',
|
||||
paint: { 'fill-color': '#f97316', 'fill-opacity': 0.4 },
|
||||
})
|
||||
map.addLayer({
|
||||
id: 'dataset-line',
|
||||
type: 'line',
|
||||
source: 'dataset',
|
||||
paint: { 'line-color': '#ea580c', 'line-width': 2 },
|
||||
})
|
||||
}
|
||||
|
||||
if (data) {
|
||||
const collection = data
|
||||
if (collection.type === 'FeatureCollection' && collection.features.length > 0) {
|
||||
const bounds = collectCoordinates(collection)
|
||||
if (bounds) {
|
||||
map.fitBounds(bounds, { padding: 40 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [data])
|
||||
|
||||
return <div className="map-container" ref={containerRef} />
|
||||
}
|
||||
|
||||
export default GeoMap
|
||||
@@ -0,0 +1,241 @@
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionModelCapability,
|
||||
DetectionQaResult,
|
||||
DetectionRead,
|
||||
DetectionRunRead,
|
||||
DetectionRunResponse,
|
||||
} from '../../types'
|
||||
|
||||
interface DetectionLabProps {
|
||||
detectionModels: DetectionModelCapability[]
|
||||
loadingDetectionModels: boolean
|
||||
detectionModelError: string | null
|
||||
selectedDetectionDatasetId: string
|
||||
selectedDetectionModelId: string
|
||||
detectionTileManifestPath: string
|
||||
detectionConfidenceThreshold: number
|
||||
runningDetection: boolean
|
||||
detectionRunResult: DetectionRunResponse | null
|
||||
detectionRunError: string | null
|
||||
detectionRuns: DetectionRunRead[]
|
||||
selectedDetectionRunId: string
|
||||
detectionItems: DetectionRead[]
|
||||
detectionClassFilter: string
|
||||
detectionMinConfidenceFilter: number
|
||||
loadingDetectionResults: boolean
|
||||
detectionReferenceDatasetId: string
|
||||
detectionQaResult: DetectionQaResult | null
|
||||
detectionQaError: string | null
|
||||
runningDetectionQa: boolean
|
||||
selectedProjectId: string | null
|
||||
rasterDatasets: DatasetCreateResponse[]
|
||||
referenceDatasets: DatasetCreateResponse[]
|
||||
onLoadModels: () => void
|
||||
onSelectDataset: (datasetId: string) => void
|
||||
onSelectModel: (modelId: string) => void
|
||||
onSetConfidenceThreshold: (value: number) => void
|
||||
onSetTileManifestPath: (value: string) => void
|
||||
onRunDetection: () => void
|
||||
onLoadRuns: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
onSetClassFilter: (value: string) => void
|
||||
onSetMinConfidenceFilter: (value: number) => void
|
||||
onLoadResults: () => void
|
||||
onSelectReferenceDataset: (datasetId: string) => void
|
||||
onRunQa: () => void
|
||||
}
|
||||
|
||||
export function DetectionLab({
|
||||
detectionModels,
|
||||
loadingDetectionModels,
|
||||
detectionModelError,
|
||||
selectedDetectionDatasetId,
|
||||
selectedDetectionModelId,
|
||||
detectionTileManifestPath,
|
||||
detectionConfidenceThreshold,
|
||||
runningDetection,
|
||||
detectionRunResult,
|
||||
detectionRunError,
|
||||
detectionRuns,
|
||||
selectedDetectionRunId,
|
||||
detectionItems,
|
||||
detectionClassFilter,
|
||||
detectionMinConfidenceFilter,
|
||||
loadingDetectionResults,
|
||||
detectionReferenceDatasetId,
|
||||
detectionQaResult,
|
||||
detectionQaError,
|
||||
runningDetectionQa,
|
||||
selectedProjectId,
|
||||
rasterDatasets,
|
||||
referenceDatasets,
|
||||
onLoadModels,
|
||||
onSelectDataset,
|
||||
onSelectModel,
|
||||
onSetConfidenceThreshold,
|
||||
onSetTileManifestPath,
|
||||
onRunDetection,
|
||||
onLoadRuns,
|
||||
onSelectRun,
|
||||
onSetClassFilter,
|
||||
onSetMinConfidenceFilter,
|
||||
onLoadResults,
|
||||
onSelectReferenceDataset,
|
||||
onRunQa,
|
||||
}: DetectionLabProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Detection Lab</h2>
|
||||
<button type="button" onClick={onLoadModels} disabled={loadingDetectionModels}>
|
||||
Refresh detection models
|
||||
</button>
|
||||
{loadingDetectionModels ? <p>Loading detection models...</p> : null}
|
||||
{detectionModelError ? <p className="error">{detectionModelError}</p> : null}
|
||||
{detectionModels.length === 0 && !loadingDetectionModels ? <p>No detection models reported by backend</p> : null}
|
||||
<ul>
|
||||
{detectionModels.map((model) => (
|
||||
<li key={model.model_id}>
|
||||
<strong>{model.display_name}</strong>
|
||||
<div>model: {model.model_id}</div>
|
||||
<div>framework: {model.framework}</div>
|
||||
<div>task: {model.task_type}</div>
|
||||
<div>status: {model.status}</div>
|
||||
<div>configured: {model.configured ? 'yes' : 'no'}</div>
|
||||
<div>classes: {model.supported_classes.join(', ')}</div>
|
||||
<div>limitation: {model.limitation_message}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
||||
<option value="">Select raster dataset</option>
|
||||
{rasterDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}>
|
||||
{detectionModels.map((model) => (
|
||||
<option key={model.model_id} value={model.model_id}>
|
||||
{model.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={detectionConfidenceThreshold}
|
||||
onChange={(event) => onSetConfidenceThreshold(Number(event.target.value))}
|
||||
/>
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Raster tile manifest path"
|
||||
value={detectionTileManifestPath}
|
||||
onChange={(event) => onSetTileManifestPath(event.target.value)}
|
||||
/>
|
||||
) : null}
|
||||
<button type="button" onClick={onRunDetection} disabled={runningDetection || !selectedProjectId || rasterDatasets.length === 0}>
|
||||
Run detection
|
||||
</button>
|
||||
</div>
|
||||
{detectionRunError ? <p className="error">{detectionRunError}</p> : null}
|
||||
{detectionRunResult ? (
|
||||
<div>
|
||||
<p>Status: {detectionRunResult.status}</p>
|
||||
<p>Message: {detectionRunResult.message}</p>
|
||||
<p>Analysis run: {detectionRunResult.analysis_run_id}</p>
|
||||
<p>Job: {detectionRunResult.job_id}</p>
|
||||
<p>Detections: {detectionRunResult.detection_count}</p>
|
||||
{detectionRunResult.error_code ? <p className="error">Code: {detectionRunResult.error_code}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<h3>Detection results</h3>
|
||||
<button type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Refresh detection runs
|
||||
</button>
|
||||
<select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}>
|
||||
<option value="">Select detection run</option>
|
||||
{detectionRuns.map((run) => (
|
||||
<option key={run.id} value={run.id}>
|
||||
{run.model_name || 'detection'} - {run.status} - {run.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Class filter"
|
||||
value={detectionClassFilter}
|
||||
onChange={(event) => onSetClassFilter(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={detectionMinConfidenceFilter}
|
||||
onChange={(event) => onSetMinConfidenceFilter(Number(event.target.value))}
|
||||
/>
|
||||
<button type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
|
||||
Load detections
|
||||
</button>
|
||||
{loadingDetectionResults ? <p>Loading detection results...</p> : null}
|
||||
<p>Detections loaded: {detectionItems.length}</p>
|
||||
{detectionItems.length > 0 ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Model</th>
|
||||
<th>Source tile</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{detectionItems.map((detection) => (
|
||||
<tr key={detection.id}>
|
||||
<td>{detection.class_name}</td>
|
||||
<td>{detection.confidence.toFixed(2)}</td>
|
||||
<td>{detection.model_name}</td>
|
||||
<td>{detection.source_tile_path || 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Detection QA</h3>
|
||||
<select value={detectionReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||
<option value="">Select reference dataset</option>
|
||||
{referenceDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
|
||||
Compare detections to reference
|
||||
</button>
|
||||
{detectionQaError ? <p className="error">{detectionQaError}</p> : null}
|
||||
{detectionQaResult ? (
|
||||
<div>
|
||||
<p>Status: {detectionQaResult.status}</p>
|
||||
<p>Quality check: {detectionQaResult.quality_check_id}</p>
|
||||
<p>Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Recall: {detectionQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>F1: {detectionQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Mean IoU: {detectionQaResult.mean_iou?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>False positives: {detectionQaResult.false_positives}</p>
|
||||
<p>False negatives: {detectionQaResult.false_negatives}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { DatasetCreateResponse, ExportCreateResponse, ExportRead } from '../../types'
|
||||
|
||||
interface ExportCenterProps {
|
||||
selectedProjectId: string | null
|
||||
selectedDataset: DatasetCreateResponse | null
|
||||
selectedDetectionRunId: string
|
||||
selectedSegmentationRunId: string
|
||||
exports: ExportRead[]
|
||||
latestExport: ExportCreateResponse | null
|
||||
exportError: string | null
|
||||
loadingExports: boolean
|
||||
exporting: boolean
|
||||
onRefresh: () => void
|
||||
onExportDataset: () => void
|
||||
onExportDetectionRun: () => void
|
||||
onExportSegmentationRun: () => void
|
||||
onExportProjectMetadata: () => void
|
||||
onExportProjectReport: () => void
|
||||
onPreviewContent: (exportId: string) => void
|
||||
onDownload: (exportId: string) => void
|
||||
}
|
||||
|
||||
function isVectorDatasetType(datasetType: string): boolean {
|
||||
return datasetType === 'vector' || datasetType === 'geojson'
|
||||
}
|
||||
|
||||
export function ExportCenter({
|
||||
selectedProjectId,
|
||||
selectedDataset,
|
||||
selectedDetectionRunId,
|
||||
selectedSegmentationRunId,
|
||||
exports,
|
||||
latestExport,
|
||||
exportError,
|
||||
loadingExports,
|
||||
exporting,
|
||||
onRefresh,
|
||||
onExportDataset,
|
||||
onExportDetectionRun,
|
||||
onExportSegmentationRun,
|
||||
onExportProjectMetadata,
|
||||
onExportProjectReport,
|
||||
onPreviewContent,
|
||||
onDownload,
|
||||
}: ExportCenterProps): JSX.Element {
|
||||
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Export Center</h2>
|
||||
<button type="button" onClick={onRefresh} disabled={!selectedProjectId || loadingExports}>
|
||||
Refresh exports
|
||||
</button>
|
||||
<button type="button" onClick={onExportProjectMetadata} disabled={!selectedProjectId || exporting}>
|
||||
Export project metadata JSON
|
||||
</button>
|
||||
<button type="button" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
||||
Export project report HTML
|
||||
</button>
|
||||
<button type="button" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
|
||||
Export selected vector GeoJSON
|
||||
</button>
|
||||
<button type="button" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
|
||||
Export selected detection run GeoJSON
|
||||
</button>
|
||||
<button type="button" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
|
||||
Export selected segmentation run GeoJSON
|
||||
</button>
|
||||
{exportError ? <p className="error">{exportError}</p> : null}
|
||||
{latestExport ? (
|
||||
<p>
|
||||
Latest export: {latestExport.export_type} {'->'} {latestExport.path}
|
||||
</p>
|
||||
) : null}
|
||||
{exports.length === 0 ? <p>No exports registered yet.</p> : null}
|
||||
<ul>
|
||||
{exports.map((item) => (
|
||||
<li key={item.id}>
|
||||
<strong>{item.export_type}</strong>
|
||||
<div>status: {item.status}</div>
|
||||
<div>path: {item.storage_path}</div>
|
||||
<div>export id: {item.id}</div>
|
||||
<button type="button" onClick={() => onPreviewContent(item.id)}>
|
||||
Preview JSON content
|
||||
</button>
|
||||
<button type="button" onClick={() => onDownload(item.id)}>
|
||||
Download artifact
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { AreaRead, ProjectRead } from '../../types'
|
||||
|
||||
interface AreaFormState {
|
||||
name: string
|
||||
geometry: string
|
||||
crs: string
|
||||
}
|
||||
|
||||
interface AreaPanelProps {
|
||||
areas: AreaRead[]
|
||||
selectedProject: ProjectRead | null
|
||||
selectedProjectId: string | null
|
||||
loadingAreas: boolean
|
||||
areaForm: AreaFormState
|
||||
onCreateArea: (event: FormEvent<HTMLFormElement>) => void
|
||||
onUpdateAreaForm: (areaForm: AreaFormState) => void
|
||||
}
|
||||
|
||||
export function AreaPanel({
|
||||
areas,
|
||||
selectedProject,
|
||||
selectedProjectId,
|
||||
loadingAreas,
|
||||
areaForm,
|
||||
onCreateArea,
|
||||
onUpdateAreaForm,
|
||||
}: AreaPanelProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Area manager</h2>
|
||||
<p>{selectedProject ? `Selected project: ${selectedProject.name}` : 'Select a project first'}</p>
|
||||
|
||||
<form onSubmit={onCreateArea}>
|
||||
<input
|
||||
value={areaForm.name}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, name: event.target.value })}
|
||||
placeholder="AOI name"
|
||||
/>
|
||||
<input
|
||||
value={areaForm.crs}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, crs: event.target.value })}
|
||||
placeholder="EPSG:4326"
|
||||
/>
|
||||
<textarea
|
||||
value={areaForm.geometry}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, geometry: event.target.value })}
|
||||
rows={4}
|
||||
/>
|
||||
<button type="submit" disabled={!selectedProjectId}>
|
||||
Create area
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{loadingAreas ? <p>Loading areas...</p> : null}
|
||||
{areas.length === 0 ? <p>No areas yet</p> : null}
|
||||
<ul>
|
||||
{areas.map((area) => (
|
||||
<li key={area.id}>
|
||||
{area.name} · {area.area_m2 ? `${area.area_m2.toFixed(2)} m²` : 'n/a'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { ProjectCreate, ProjectRead } from '../../types'
|
||||
|
||||
interface ProjectPanelProps {
|
||||
projects: ProjectRead[]
|
||||
selectedProjectId: string | null
|
||||
loadingProjects: boolean
|
||||
projectForm: ProjectCreate
|
||||
loadingDemoWorkflow: boolean
|
||||
demoWorkflowMessage: string | null
|
||||
onCreateProject: (event: FormEvent<HTMLFormElement>) => void
|
||||
onUpdateProjectForm: (projectForm: ProjectCreate) => void
|
||||
onSelectProject: (projectId: string) => void
|
||||
onLoadDemoWorkflow: () => void
|
||||
}
|
||||
|
||||
export function ProjectPanel({
|
||||
projects,
|
||||
selectedProjectId,
|
||||
loadingProjects,
|
||||
projectForm,
|
||||
loadingDemoWorkflow,
|
||||
demoWorkflowMessage,
|
||||
onCreateProject,
|
||||
onUpdateProjectForm,
|
||||
onSelectProject,
|
||||
onLoadDemoWorkflow,
|
||||
}: ProjectPanelProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Projects</h2>
|
||||
{loadingProjects ? <p>Loading projects...</p> : null}
|
||||
|
||||
<form onSubmit={onCreateProject}>
|
||||
<input
|
||||
value={projectForm.name}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, name: event.target.value })}
|
||||
placeholder="Project name"
|
||||
/>
|
||||
<input
|
||||
value={projectForm.description ?? ''}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, description: event.target.value })}
|
||||
placeholder="Description"
|
||||
/>
|
||||
<input
|
||||
value={projectForm.region ?? 'Kempen'}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
|
||||
placeholder="Region"
|
||||
/>
|
||||
<button type="submit">Create project</button>
|
||||
</form>
|
||||
|
||||
<div className="demo-actions">
|
||||
<button type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow}>
|
||||
{loadingDemoWorkflow ? 'Loading demo...' : 'Load demo workflow'}
|
||||
</button>
|
||||
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
{projects.map((project) => (
|
||||
<li key={project.id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelectProject(project.id)}
|
||||
aria-pressed={project.id === selectedProjectId}
|
||||
>
|
||||
{project.name}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{projects.length === 0 ? <li>No projects yet</li> : null}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ProviderCapability } from '../../types'
|
||||
|
||||
interface ProviderPanelProps {
|
||||
providers: ProviderCapability[]
|
||||
loadingCapabilities: boolean
|
||||
capabilitiesError: string | null
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function ProviderPanel({
|
||||
providers,
|
||||
loadingCapabilities,
|
||||
capabilitiesError,
|
||||
onRefresh,
|
||||
}: ProviderPanelProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Provider Capabilities</h2>
|
||||
<button type="button" onClick={onRefresh} disabled={loadingCapabilities}>
|
||||
Refresh providers
|
||||
</button>
|
||||
{loadingCapabilities ? <p>Loading provider capabilities...</p> : null}
|
||||
{capabilitiesError ? <p className="error">{capabilitiesError}</p> : null}
|
||||
{providers.length === 0 && !loadingCapabilities ? <p>No providers reported by backend</p> : null}
|
||||
<ul>
|
||||
{providers.map((provider) => (
|
||||
<li key={provider.provider_name}>
|
||||
<strong>{provider.display_name}</strong>
|
||||
<div>provider: {provider.provider_name}</div>
|
||||
<div>authority: {provider.authority_level}</div>
|
||||
<div>status: {provider.status}</div>
|
||||
<div>configured: {provider.configured ? 'yes' : 'no'}</div>
|
||||
<div>layers: {provider.supported_layers.join(', ')}</div>
|
||||
<div>geometry: {provider.supported_geometry_types.join(', ')}</div>
|
||||
<div>query modes: {provider.supported_query_modes.join(', ')}</div>
|
||||
<div>limitation: {provider.limitation_message}</div>
|
||||
<div>attribution: {provider.attribution}</div>
|
||||
<div>license: {provider.license_note}</div>
|
||||
{!provider.configured && provider.not_configured_reason ? (
|
||||
<div>reason: {provider.not_configured_reason}</div>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
SegmentationModelCapability,
|
||||
SegmentationQaResult,
|
||||
SegmentationRead,
|
||||
SegmentationRunRead,
|
||||
SegmentationRunResponse,
|
||||
} from '../../types'
|
||||
|
||||
interface SegmentationLabProps {
|
||||
segmentationModels: SegmentationModelCapability[]
|
||||
loadingSegmentationModels: boolean
|
||||
segmentationModelError: string | null
|
||||
selectedSegmentationDatasetId: string
|
||||
selectedSegmentationModelId: string
|
||||
segmentationConfidenceThreshold: number
|
||||
runningSegmentation: boolean
|
||||
segmentationRunResult: SegmentationRunResponse | null
|
||||
segmentationRunError: string | null
|
||||
segmentationRuns: SegmentationRunRead[]
|
||||
selectedSegmentationRunId: string
|
||||
segmentationItems: SegmentationRead[]
|
||||
segmentationClassFilter: string
|
||||
segmentationMinConfidenceFilter: number
|
||||
loadingSegmentationResults: boolean
|
||||
segmentationReferenceDatasetId: string
|
||||
segmentationQaResult: SegmentationQaResult | null
|
||||
segmentationQaError: string | null
|
||||
runningSegmentationQa: boolean
|
||||
selectedProjectId: string | null
|
||||
rasterDatasets: DatasetCreateResponse[]
|
||||
referenceDatasets: DatasetCreateResponse[]
|
||||
selectedSegmentationModelConfigured: boolean
|
||||
selectedSegmentationModelLimitation: string | null
|
||||
onLoadModels: () => void
|
||||
onSelectDataset: (datasetId: string) => void
|
||||
onSelectModel: (modelId: string) => void
|
||||
onSetConfidenceThreshold: (value: number) => void
|
||||
onRunSegmentation: () => void
|
||||
onLoadRuns: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
onSetClassFilter: (value: string) => void
|
||||
onSetMinConfidenceFilter: (value: number) => void
|
||||
onLoadResults: () => void
|
||||
onSelectReferenceDataset: (datasetId: string) => void
|
||||
onRunQa: () => void
|
||||
}
|
||||
|
||||
export function SegmentationLab({
|
||||
segmentationModels,
|
||||
loadingSegmentationModels,
|
||||
segmentationModelError,
|
||||
selectedSegmentationDatasetId,
|
||||
selectedSegmentationModelId,
|
||||
segmentationConfidenceThreshold,
|
||||
runningSegmentation,
|
||||
segmentationRunResult,
|
||||
segmentationRunError,
|
||||
segmentationRuns,
|
||||
selectedSegmentationRunId,
|
||||
segmentationItems,
|
||||
segmentationClassFilter,
|
||||
segmentationMinConfidenceFilter,
|
||||
loadingSegmentationResults,
|
||||
segmentationReferenceDatasetId,
|
||||
segmentationQaResult,
|
||||
segmentationQaError,
|
||||
runningSegmentationQa,
|
||||
selectedProjectId,
|
||||
rasterDatasets,
|
||||
referenceDatasets,
|
||||
selectedSegmentationModelConfigured,
|
||||
selectedSegmentationModelLimitation,
|
||||
onLoadModels,
|
||||
onSelectDataset,
|
||||
onSelectModel,
|
||||
onSetConfidenceThreshold,
|
||||
onRunSegmentation,
|
||||
onLoadRuns,
|
||||
onSelectRun,
|
||||
onSetClassFilter,
|
||||
onSetMinConfidenceFilter,
|
||||
onLoadResults,
|
||||
onSelectReferenceDataset,
|
||||
onRunQa,
|
||||
}: SegmentationLabProps): JSX.Element {
|
||||
return (
|
||||
<section>
|
||||
<h2>Segmentation Lab</h2>
|
||||
<button type="button" onClick={onLoadModels} disabled={loadingSegmentationModels}>
|
||||
Refresh segmentation models
|
||||
</button>
|
||||
{loadingSegmentationModels ? <p>Loading segmentation models...</p> : null}
|
||||
{segmentationModelError ? <p className="error">{segmentationModelError}</p> : null}
|
||||
{segmentationModels.length === 0 && !loadingSegmentationModels ? <p>No segmentation models reported by backend</p> : null}
|
||||
<ul>
|
||||
{segmentationModels.map((model) => (
|
||||
<li key={model.model_id}>
|
||||
<strong>{model.display_name}</strong>
|
||||
<div>model: {model.model_id}</div>
|
||||
<div>framework: {model.framework}</div>
|
||||
<div>task: {model.task_type}</div>
|
||||
<div>status: {model.status}</div>
|
||||
<div>configured: {model.configured ? 'yes' : 'no'}</div>
|
||||
<div>classes: {model.supported_classes.join(', ')}</div>
|
||||
<div>limitation: {model.limitation_message}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<select value={selectedSegmentationDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
||||
<option value="">Select raster dataset</option>
|
||||
{rasterDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={selectedSegmentationModelId} onChange={(event) => onSelectModel(event.target.value)}>
|
||||
{segmentationModels.map((model) => (
|
||||
<option key={model.model_id} value={model.model_id}>
|
||||
{model.display_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={segmentationConfidenceThreshold}
|
||||
onChange={(event) => onSetConfidenceThreshold(Number(event.target.value))}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRunSegmentation}
|
||||
disabled={runningSegmentation || !selectedProjectId || rasterDatasets.length === 0 || !selectedSegmentationModelConfigured}
|
||||
>
|
||||
Run segmentation
|
||||
</button>
|
||||
</div>
|
||||
{!selectedSegmentationModelConfigured ? (
|
||||
<p>{selectedSegmentationModelLimitation ?? 'Select a configured segmentation model'}</p>
|
||||
) : null}
|
||||
{segmentationRunError ? <p className="error">{segmentationRunError}</p> : null}
|
||||
{segmentationRunResult ? (
|
||||
<div>
|
||||
<p>Status: {segmentationRunResult.status}</p>
|
||||
<p>Message: {segmentationRunResult.message}</p>
|
||||
<p>Analysis run: {segmentationRunResult.analysis_run_id}</p>
|
||||
<p>Job: {segmentationRunResult.job_id}</p>
|
||||
<p>Segmentations: {segmentationRunResult.segmentation_count}</p>
|
||||
{segmentationRunResult.error_code ? <p className="error">Code: {segmentationRunResult.error_code}</p> : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<h3>Segmentation results</h3>
|
||||
<button type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Refresh segmentation runs
|
||||
</button>
|
||||
<select value={selectedSegmentationRunId} onChange={(event) => onSelectRun(event.target.value)}>
|
||||
<option value="">Select segmentation run</option>
|
||||
{segmentationRuns.map((run) => (
|
||||
<option key={run.id} value={run.id}>
|
||||
{run.model_name || 'segmentation'} - {run.status} - {run.id}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Class filter"
|
||||
value={segmentationClassFilter}
|
||||
onChange={(event) => onSetClassFilter(event.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={segmentationMinConfidenceFilter}
|
||||
onChange={(event) => onSetMinConfidenceFilter(Number(event.target.value))}
|
||||
/>
|
||||
<button type="button" onClick={onLoadResults} disabled={!selectedSegmentationRunId || loadingSegmentationResults}>
|
||||
Load segmentations
|
||||
</button>
|
||||
{loadingSegmentationResults ? <p>Loading segmentation results...</p> : null}
|
||||
<p>Segmentations loaded: {segmentationItems.length}</p>
|
||||
{segmentationItems.length > 0 ? (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Area m2</th>
|
||||
<th>Model</th>
|
||||
<th>Tile</th>
|
||||
<th>Mask path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{segmentationItems.map((segmentation) => (
|
||||
<tr key={segmentation.id}>
|
||||
<td>{segmentation.class_name}</td>
|
||||
<td>{segmentation.confidence?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.area_m2?.toFixed(2) ?? 'n/a'}</td>
|
||||
<td>{segmentation.model_name}</td>
|
||||
<td>{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}</td>
|
||||
<td>{segmentation.mask_path || 'n/a'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<h3>Segmentation QA</h3>
|
||||
<select value={segmentationReferenceDatasetId} onChange={(event) => onSelectReferenceDataset(event.target.value)}>
|
||||
<option value="">Select reference dataset</option>
|
||||
{referenceDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="button" onClick={onRunQa} disabled={runningSegmentationQa || !selectedSegmentationRunId || !segmentationReferenceDatasetId}>
|
||||
Compare segmentations to reference
|
||||
</button>
|
||||
{segmentationQaError ? <p className="error">{segmentationQaError}</p> : null}
|
||||
{segmentationQaResult ? (
|
||||
<div>
|
||||
<p>Status: {segmentationQaResult.status}</p>
|
||||
<p>Quality check: {segmentationQaResult.quality_check_id}</p>
|
||||
<p>Precision: {segmentationQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Recall: {segmentationQaResult.recall?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>F1: {segmentationQaResult.f1_score?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>Mean IoU: {segmentationQaResult.mean_iou?.toFixed(3) ?? 'n/a'}</p>
|
||||
<p>False positives: {segmentationQaResult.false_positives}</p>
|
||||
<p>False negatives: {segmentationQaResult.false_negatives}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './styles/app.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
import { apiGet, apiPost, apiPatch } from './client'
|
||||
import type { AreaCreate, AreaListResponse, AreaRead } from '../../types'
|
||||
|
||||
export const areasApi = {
|
||||
list: (projectId: string): Promise<AreaListResponse> => apiGet<AreaListResponse>(`/api/v1/projects/${projectId}/areas`),
|
||||
create: (projectId: string, payload: AreaCreate): Promise<AreaRead> =>
|
||||
apiPost<AreaRead>(`/api/v1/projects/${projectId}/areas`, payload),
|
||||
get: (projectId: string, areaId: string): Promise<AreaRead> =>
|
||||
apiGet<AreaRead>(`/api/v1/projects/${projectId}/areas/${areaId}`),
|
||||
update: (projectId: string, areaId: string, payload: Partial<AreaCreate>): Promise<AreaRead> =>
|
||||
apiPatch<AreaRead>(`/api/v1/projects/${projectId}/areas/${areaId}`, payload),
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "";
|
||||
|
||||
export function apiUrl(path: string): string {
|
||||
return `${API_BASE_URL}${path}`;
|
||||
}
|
||||
|
||||
export class ApiHttpError extends Error {
|
||||
readonly code: string;
|
||||
readonly details?: unknown;
|
||||
|
||||
constructor(message: string, code = "REQUEST_ERROR", details?: unknown) {
|
||||
super(message);
|
||||
this.name = "ApiHttpError";
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
async function parseResponse<T>(response: Response): Promise<T> {
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
const code = payload?.error?.code ?? "REQUEST_ERROR";
|
||||
const message = payload?.error?.message ?? `Request failed (${response.status})`;
|
||||
const details = payload?.error?.details;
|
||||
throw new ApiHttpError(message, code, details);
|
||||
}
|
||||
return payload.data as T;
|
||||
}
|
||||
|
||||
export async function apiGet<T>(path: string): Promise<T> {
|
||||
const response = await fetch(apiUrl(path));
|
||||
return parseResponse<T>(response);
|
||||
}
|
||||
|
||||
export async function apiPost<T>(path: string, body?: object): Promise<T> {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
return parseResponse<T>(response);
|
||||
}
|
||||
|
||||
export async function apiPatch<T>(path: string, body?: object): Promise<T> {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
return parseResponse<T>(response);
|
||||
}
|
||||
|
||||
export async function apiDelete<T>(path: string): Promise<T> {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
method: "DELETE",
|
||||
});
|
||||
return parseResponse<T>(response);
|
||||
}
|
||||
|
||||
export async function apiMultipart<T>(path: string, form: FormData): Promise<T> {
|
||||
const response = await fetch(apiUrl(path), {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
return parseResponse<T>(response);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { apiGet, apiMultipart, apiPost } from './client'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DatasetListResponse,
|
||||
RasterMetadataResponse,
|
||||
RasterInspectResponse,
|
||||
RasterStatsResponse,
|
||||
RasterPreviewResponse,
|
||||
JobRead,
|
||||
VectorBBoxResponse,
|
||||
VectorStatsResponse,
|
||||
VectorSummary,
|
||||
RasterNdviRequest,
|
||||
RasterNdwiRequest,
|
||||
RasterNdbiRequest,
|
||||
} from '../../types'
|
||||
|
||||
export const datasetsApi = {
|
||||
list: (projectId: string): Promise<DatasetListResponse> =>
|
||||
apiGet<DatasetListResponse>(`/api/v1/projects/${projectId}/datasets`),
|
||||
upload: (
|
||||
projectId: string,
|
||||
payload: {
|
||||
file: File
|
||||
datasetType: string
|
||||
source: string
|
||||
datasetRole: string
|
||||
sourceName?: string
|
||||
referenceLayerName?: string
|
||||
sourceMetadataJson?: string
|
||||
provenanceMetadataJson?: string
|
||||
areaId?: string
|
||||
},
|
||||
): Promise<DatasetCreateResponse> => {
|
||||
const form = new FormData()
|
||||
form.append('file', payload.file)
|
||||
form.append('dataset_type', payload.datasetType)
|
||||
form.append('source', payload.source)
|
||||
form.append('dataset_role', payload.datasetRole)
|
||||
if (payload.sourceName) {
|
||||
form.append('source_name', payload.sourceName)
|
||||
}
|
||||
if (payload.referenceLayerName) {
|
||||
form.append('reference_layer_name', payload.referenceLayerName)
|
||||
}
|
||||
if (payload.sourceMetadataJson) {
|
||||
form.append('source_metadata_json', payload.sourceMetadataJson)
|
||||
}
|
||||
if (payload.provenanceMetadataJson) {
|
||||
form.append('provenance_metadata_json', payload.provenanceMetadataJson)
|
||||
}
|
||||
if (payload.areaId) {
|
||||
form.append('area_id', payload.areaId)
|
||||
}
|
||||
return apiMultipart<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/upload`, form)
|
||||
},
|
||||
refreshMetadata: (projectId: string, datasetId: string): Promise<DatasetCreateResponse> =>
|
||||
apiPost<DatasetCreateResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/metadata/refresh`, {}),
|
||||
inspectRaster: (projectId: string, datasetId: string): Promise<RasterInspectResponse> =>
|
||||
apiGet<RasterInspectResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/inspect`),
|
||||
rasterStats: (projectId: string, datasetId: string): Promise<RasterStatsResponse> =>
|
||||
apiGet<RasterStatsResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/stats`),
|
||||
inspectVector: (projectId: string, datasetId: string): Promise<{ dataset: DatasetCreateResponse; summary: VectorSummary | null; metadata: unknown }> =>
|
||||
apiGet<{ dataset: DatasetCreateResponse; summary: VectorSummary | null; metadata: unknown }>(
|
||||
`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/inspect`,
|
||||
),
|
||||
vectorBbox: (projectId: string, datasetId: string): Promise<VectorBBoxResponse> =>
|
||||
apiGet<VectorBBoxResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/bbox`),
|
||||
vectorSummary: (projectId: string, datasetId: string): Promise<VectorSummary> =>
|
||||
apiGet<VectorSummary>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/summary`),
|
||||
vectorStats: (projectId: string, datasetId: string): Promise<VectorStatsResponse> =>
|
||||
apiGet<VectorStatsResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/stats`),
|
||||
vectorClip: (projectId: string, datasetId: string, payload: { area_id: string; output_name?: string }) =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/clip`, payload),
|
||||
vectorBuffer: (projectId: string, datasetId: string, payload: { distance_m: number; dissolve?: boolean; output_name?: string }) =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/buffer`, payload),
|
||||
vectorIntersect: (projectId: string, datasetId: string, payload: { other_dataset_id: string; output_name?: string }) =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/vector/intersect`, payload),
|
||||
rasterMetadata: (projectId: string, datasetId: string): Promise<RasterMetadataResponse> =>
|
||||
apiGet<RasterMetadataResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/metadata`),
|
||||
rasterInspect: (projectId: string, datasetId: string): Promise<RasterInspectResponse> =>
|
||||
apiGet<RasterInspectResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/inspect`),
|
||||
rasterTile: (
|
||||
projectId: string,
|
||||
datasetId: string,
|
||||
payload: { tile_size?: number; overlap?: number; output_name?: string } = {},
|
||||
): Promise<JobRead> =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/tile`, payload),
|
||||
rasterReproject: (
|
||||
projectId: string,
|
||||
datasetId: string,
|
||||
payload: { target_crs: string; resampling?: string; output_name?: string },
|
||||
): Promise<JobRead> => apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/reproject`, payload),
|
||||
rasterNdvi: (projectId: string, datasetId: string, payload: RasterNdviRequest): Promise<JobRead> =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/indices/ndvi`, payload),
|
||||
rasterNdwi: (projectId: string, datasetId: string, payload: RasterNdwiRequest): Promise<JobRead> =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/indices/ndwi`, payload),
|
||||
rasterNdbi: (projectId: string, datasetId: string, payload: RasterNdbiRequest): Promise<JobRead> =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/indices/ndbi`, payload),
|
||||
rasterPreview: (projectId: string, datasetId: string): Promise<RasterPreviewResponse> =>
|
||||
apiGet<RasterPreviewResponse>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/preview`),
|
||||
rasterClip: (
|
||||
projectId: string,
|
||||
datasetId: string,
|
||||
payload: { area_id: string; output_name?: string },
|
||||
): Promise<JobRead> => apiPost<JobRead>(`/api/v1/projects/${projectId}/datasets/${datasetId}/raster/clip`, payload),
|
||||
getContent: (projectId: string, datasetId: string): Promise<GeoJSON.FeatureCollection> =>
|
||||
apiGet<GeoJSON.FeatureCollection>(`/api/v1/projects/${projectId}/datasets/${datasetId}/content`),
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { apiPost } from './client'
|
||||
import type { DemoWorkflowResponse } from '../../types'
|
||||
|
||||
export const demoApi = {
|
||||
seedWorkflow: (): Promise<DemoWorkflowResponse> => apiPost<DemoWorkflowResponse>('/api/v1/demo/workflow'),
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { apiGet, apiPost } from './client'
|
||||
import type {
|
||||
DetectionListResponse,
|
||||
DetectionModelsResponse,
|
||||
DetectionQaRequest,
|
||||
DetectionQaResult,
|
||||
DetectionRunListResponse,
|
||||
DetectionRunRead,
|
||||
DetectionRunRequest,
|
||||
DetectionRunResponse,
|
||||
} from '../../types'
|
||||
|
||||
function queryString(params: Record<string, string | number | null | undefined>): string {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
searchParams.set(key, String(value))
|
||||
}
|
||||
})
|
||||
const query = searchParams.toString()
|
||||
return query ? `?${query}` : ''
|
||||
}
|
||||
|
||||
export const detectionApi = {
|
||||
listModels: (): Promise<DetectionModelsResponse> => apiGet<DetectionModelsResponse>('/api/v1/detection/models'),
|
||||
run: (payload: DetectionRunRequest): Promise<DetectionRunResponse> =>
|
||||
apiPost<DetectionRunResponse>('/api/v1/detection/run', payload),
|
||||
listRuns: (params: { project_id?: string | null; dataset_id?: string | null } = {}): Promise<DetectionRunListResponse> =>
|
||||
apiGet<DetectionRunListResponse>(`/api/v1/detection/runs${queryString(params)}`),
|
||||
getRun: (analysisRunId: string): Promise<DetectionRunRead> =>
|
||||
apiGet<DetectionRunRead>(`/api/v1/detection/runs/${analysisRunId}`),
|
||||
listDetections: (
|
||||
analysisRunId: string,
|
||||
params: { dataset_id?: string | null; class_name?: string | null; min_confidence?: number | null } = {},
|
||||
): Promise<DetectionListResponse> =>
|
||||
apiGet<DetectionListResponse>(`/api/v1/detection/runs/${analysisRunId}/detections${queryString(params)}`),
|
||||
getRunGeoJson: (
|
||||
analysisRunId: string,
|
||||
params: { class_name?: string | null; min_confidence?: number | null } = {},
|
||||
): Promise<GeoJSON.FeatureCollection> =>
|
||||
apiGet<GeoJSON.FeatureCollection>(`/api/v1/detection/runs/${analysisRunId}/geojson${queryString(params)}`),
|
||||
compareWithReference: (analysisRunId: string, payload: DetectionQaRequest): Promise<DetectionQaResult> =>
|
||||
apiPost<DetectionQaResult>(`/api/v1/detection/runs/${analysisRunId}/qa/reference`, payload),
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { apiGet, apiPost, apiUrl } from './client'
|
||||
import type { ExportContentResponse, ExportCreateResponse, ExportKind, ExportListResponse, ExportRead } from '../../types'
|
||||
|
||||
export const exportsApi = {
|
||||
exportGeojson: (
|
||||
payload: { dataset_id?: string; analysis_run_id?: string; export_kind?: ExportKind; name?: string } | string,
|
||||
): Promise<ExportCreateResponse> => {
|
||||
const body = typeof payload === 'string' ? { dataset_id: payload, export_kind: 'dataset' } : payload
|
||||
return apiPost<ExportCreateResponse>(`/api/v1/exports/geojson`, body)
|
||||
},
|
||||
exportProjectMetadata: (projectId: string, name?: string): Promise<ExportCreateResponse> =>
|
||||
apiPost<ExportCreateResponse>(`/api/v1/exports/metadata`, { project_id: projectId, name }),
|
||||
exportProjectReport: (projectId: string, name?: string): Promise<ExportCreateResponse> =>
|
||||
apiPost<ExportCreateResponse>(`/api/v1/exports/report`, { project_id: projectId, name }),
|
||||
listProjectExports: (projectId: string): Promise<ExportListResponse> =>
|
||||
apiGet<ExportListResponse>(`/api/v1/exports/projects/${projectId}/exports`),
|
||||
getExport: (exportId: string): Promise<ExportRead> => apiGet<ExportRead>(`/api/v1/exports/${exportId}`),
|
||||
getContent: (exportId: string): Promise<ExportContentResponse> =>
|
||||
apiGet<ExportContentResponse>(`/api/v1/exports/${exportId}/content`),
|
||||
downloadUrl: (exportId: string): string => apiUrl(`/api/v1/exports/${exportId}/download`),
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { apiGet, apiPost } from './client'
|
||||
import type {
|
||||
ProviderCapability,
|
||||
ProviderCapabilitiesResponse,
|
||||
ProviderImportResponse,
|
||||
ProviderLayersResponse,
|
||||
ProviderStatusResponse,
|
||||
SystemCapabilitiesResponse,
|
||||
} from '../../types'
|
||||
|
||||
const normalize = (layers: string[] = []) => layers.filter((value) => value.trim().length > 0)
|
||||
|
||||
export const externalApi = {
|
||||
listSystemCapabilities: (): Promise<SystemCapabilitiesResponse> =>
|
||||
apiGet<SystemCapabilitiesResponse>('/api/v1/system/capabilities'),
|
||||
listProviders: (): Promise<ProviderCapabilitiesResponse> =>
|
||||
apiGet<ProviderCapabilitiesResponse>('/api/v1/external/providers'),
|
||||
listProviderCapabilities: (): Promise<ProviderCapabilitiesResponse> =>
|
||||
apiGet<ProviderCapabilitiesResponse>('/api/v1/external/providers/capabilities'),
|
||||
getProvider: (providerName: string): Promise<ProviderCapability> =>
|
||||
apiGet<ProviderCapability>(`/api/v1/external/providers/${providerName}`),
|
||||
getProviderLayers: (providerName: string): Promise<ProviderLayersResponse> =>
|
||||
apiGet<ProviderLayersResponse>(`/api/v1/external/providers/${providerName}/layers`),
|
||||
getProviderStatus: (providerName: string): Promise<ProviderStatusResponse> =>
|
||||
apiGet<ProviderStatusResponse>(`/api/v1/external/providers/${providerName}/status`),
|
||||
requestProviderImport: (providerName: string, payload: {
|
||||
projectId: string
|
||||
areaId?: string
|
||||
layers: string[]
|
||||
datasetRole?: string
|
||||
}): Promise<ProviderImportResponse> =>
|
||||
apiPost<ProviderImportResponse>(`/api/v1/external/providers/${providerName}/import`, {
|
||||
project_id: payload.projectId,
|
||||
area_id: payload.areaId ?? null,
|
||||
layers: normalize(payload.layers),
|
||||
dataset_role: payload.datasetRole ?? null,
|
||||
}),
|
||||
runOsmFetch: (payload: {
|
||||
projectId: string
|
||||
areaId?: string
|
||||
layers: string[]
|
||||
}): Promise<ProviderFetchResponse> =>
|
||||
apiPost<ProviderFetchResponse>('/api/v1/external/osm/fetch', {
|
||||
project_id: payload.projectId,
|
||||
area_id: payload.areaId ?? null,
|
||||
layers: normalize(payload.layers),
|
||||
}),
|
||||
runGrbFetch: (payload: {
|
||||
projectId: string
|
||||
areaId?: string
|
||||
layers: string[]
|
||||
}): Promise<ProviderFetchResponse> =>
|
||||
apiPost<ProviderFetchResponse>('/api/v1/external/grb/fetch', {
|
||||
project_id: payload.projectId,
|
||||
area_id: payload.areaId ?? null,
|
||||
layers: normalize(payload.layers),
|
||||
}),
|
||||
}
|
||||
|
||||
export interface ProviderFetchResponse {
|
||||
provider: string
|
||||
status: string
|
||||
message: string
|
||||
requested_layers: string[]
|
||||
project_id: string
|
||||
area_id: string | null
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { areasApi } from './areas'
|
||||
export { datasetsApi } from './datasets'
|
||||
export { demoApi } from './demo'
|
||||
export { detectionApi } from './detection'
|
||||
export { externalApi } from './external'
|
||||
export { qaApi } from './qa'
|
||||
export { segmentationApi } from './segmentation'
|
||||
export { exportsApi } from './exports'
|
||||
export { jobsApi } from './jobs'
|
||||
export { projectsApi } from './projects'
|
||||
@@ -0,0 +1,31 @@
|
||||
import { apiGet, apiPost } from './client'
|
||||
import type { JobListResponse, JobRead } from '../../types'
|
||||
|
||||
export const jobsApi = {
|
||||
create: (projectId: string, payload: {
|
||||
job_type: string
|
||||
project_id: string
|
||||
dataset_id?: string | null
|
||||
input_dataset_id?: string | null
|
||||
output_dataset_id?: string | null
|
||||
parameters_json?: Record<string, unknown>
|
||||
}): Promise<JobRead> =>
|
||||
apiPost<JobRead>(`/api/v1/projects/${projectId}/jobs`, payload),
|
||||
list: (projectId: string, options?: { dataset_id?: string; limit?: number; offset?: number }): Promise<JobListResponse> => {
|
||||
const query = new URLSearchParams()
|
||||
if (options?.dataset_id) {
|
||||
query.set('dataset_id', options.dataset_id)
|
||||
}
|
||||
if (options?.limit) {
|
||||
query.set('limit', String(options.limit))
|
||||
}
|
||||
if (options?.offset) {
|
||||
query.set('offset', String(options.offset))
|
||||
}
|
||||
const queryPart = query.toString() ? `?${query}` : ''
|
||||
return apiGet<JobListResponse>(`/api/v1/projects/${projectId}/jobs${queryPart}`)
|
||||
},
|
||||
get: (projectId: string, jobId: string): Promise<JobRead> => apiGet<JobRead>(`/api/v1/projects/${projectId}/jobs/${jobId}`),
|
||||
status: (projectId: string, jobId: string): Promise<{ status: string; error_message?: string | null; started_at?: string | null; finished_at?: string | null; result_json?: Record<string, unknown> | null }> =>
|
||||
apiGet(`/api/v1/projects/${projectId}/jobs/${jobId}/status`),
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { apiDelete, apiGet, apiPatch, apiPost } from './client'
|
||||
import type { ProjectCreate, ProjectListResponse, ProjectRead } from '../../types'
|
||||
|
||||
export const projectsApi = {
|
||||
list: (): Promise<ProjectListResponse> => apiGet<ProjectListResponse>('/api/v1/projects'),
|
||||
create: (payload: ProjectCreate): Promise<ProjectRead> => apiPost<ProjectRead>('/api/v1/projects', payload),
|
||||
get: (id: string): Promise<ProjectRead> => apiGet<ProjectRead>(`/api/v1/projects/${id}`),
|
||||
update: (id: string, payload: Partial<ProjectCreate>): Promise<ProjectRead> =>
|
||||
apiPatch<ProjectRead>(`/api/v1/projects/${id}`, payload),
|
||||
delete: (id: string): Promise<{ deleted: boolean }> =>
|
||||
apiDelete<{ deleted: boolean }>(`/api/v1/projects/${id}`),
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { apiGet, apiPost } from './client'
|
||||
import type { QaComparisonRequest, JobRead, QualityCheckListResponse } from '../../types'
|
||||
|
||||
export const qaApi = {
|
||||
runQa: (payload: QaComparisonRequest): Promise<JobRead> =>
|
||||
apiPost<JobRead>('/api/v1/qa/detections-vs-reference', payload),
|
||||
listQualityChecks: (projectId: string): Promise<QualityCheckListResponse> =>
|
||||
apiGet<QualityCheckListResponse>(`/api/v1/projects/${projectId}/quality-checks`),
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { apiGet, apiPost } from './client'
|
||||
import type {
|
||||
SegmentationListResponse,
|
||||
SegmentationModelsResponse,
|
||||
SegmentationQaRequest,
|
||||
SegmentationQaResult,
|
||||
SegmentationRunListResponse,
|
||||
SegmentationRunRead,
|
||||
SegmentationRunRequest,
|
||||
SegmentationRunResponse,
|
||||
} from '../../types'
|
||||
|
||||
function queryString(params: Record<string, string | number | null | undefined>): string {
|
||||
const searchParams = new URLSearchParams()
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
searchParams.set(key, String(value))
|
||||
}
|
||||
})
|
||||
const query = searchParams.toString()
|
||||
return query ? `?${query}` : ''
|
||||
}
|
||||
|
||||
export const segmentationApi = {
|
||||
listModels: (): Promise<SegmentationModelsResponse> => apiGet<SegmentationModelsResponse>('/api/v1/segmentation/models'),
|
||||
run: (payload: SegmentationRunRequest): Promise<SegmentationRunResponse> =>
|
||||
apiPost<SegmentationRunResponse>('/api/v1/segmentation/run', payload),
|
||||
listRuns: (params: { project_id?: string | null; dataset_id?: string | null } = {}): Promise<SegmentationRunListResponse> =>
|
||||
apiGet<SegmentationRunListResponse>(`/api/v1/segmentation/runs${queryString(params)}`),
|
||||
getRun: (analysisRunId: string): Promise<SegmentationRunRead> =>
|
||||
apiGet<SegmentationRunRead>(`/api/v1/segmentation/runs/${analysisRunId}`),
|
||||
listSegmentations: (
|
||||
analysisRunId: string,
|
||||
params: { dataset_id?: string | null; class_name?: string | null; min_confidence?: number | null } = {},
|
||||
): Promise<SegmentationListResponse> =>
|
||||
apiGet<SegmentationListResponse>(`/api/v1/segmentation/runs/${analysisRunId}/segmentations${queryString(params)}`),
|
||||
getRunGeoJson: (
|
||||
analysisRunId: string,
|
||||
params: { class_name?: string | null; min_confidence?: number | null } = {},
|
||||
): Promise<GeoJSON.FeatureCollection> =>
|
||||
apiGet<GeoJSON.FeatureCollection>(`/api/v1/segmentation/runs/${analysisRunId}/geojson${queryString(params)}`),
|
||||
compareWithReference: (analysisRunId: string, payload: SegmentationQaRequest): Promise<SegmentationQaResult> =>
|
||||
apiPost<SegmentationQaResult>(`/api/v1/segmentation/runs/${analysisRunId}/qa/reference`, payload),
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
@import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
|
||||
:root {
|
||||
--bg: #0f172a;
|
||||
--panel: #ffffff;
|
||||
--muted: #475569;
|
||||
--line: #cbd5e1;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', 'Avenir Next', 'Segoe UI', sans-serif;
|
||||
color: #0f172a;
|
||||
background: radial-gradient(circle at top, #1d4ed8 0%, #1e293b 45%, #020617 100%);
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.app-shell,
|
||||
section {
|
||||
background: color-mix(in srgb, var(--panel) 92%, transparent);
|
||||
}
|
||||
|
||||
.workspace-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
button,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
margin-top: 0.4rem;
|
||||
font: inherit;
|
||||
border: 1px solid #94a3b8;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo-actions {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.1rem;
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #7f1d1d;
|
||||
background: #fee2e2;
|
||||
padding: 0.6rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 460px;
|
||||
border: 1px solid #94a3b8;
|
||||
border-radius: 8px;
|
||||
}
|
||||
@@ -0,0 +1,626 @@
|
||||
export interface ApiEnvelope<T> {
|
||||
data: T
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
code: string
|
||||
message: string
|
||||
details: Record<string, unknown> | unknown[]
|
||||
}
|
||||
|
||||
export interface ApiErrorEnvelope {
|
||||
error: ApiError
|
||||
}
|
||||
|
||||
export interface ProjectRead {
|
||||
id: string
|
||||
name: string
|
||||
description?: string | null
|
||||
region: string
|
||||
status: string
|
||||
created_at?: string | null
|
||||
updated_at?: string | null
|
||||
}
|
||||
|
||||
export interface ProjectCreate {
|
||||
name: string
|
||||
description?: string | null
|
||||
region?: string
|
||||
}
|
||||
|
||||
export interface ProjectListResponse {
|
||||
items: ProjectRead[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface DemoWorkflowResponse {
|
||||
project_id: string
|
||||
area_id: string
|
||||
reference_dataset_id: string
|
||||
candidate_dataset_id: string
|
||||
quality_check_id: string
|
||||
metric_count: number
|
||||
status: string
|
||||
message: string
|
||||
created: boolean
|
||||
}
|
||||
|
||||
export interface AreaRead {
|
||||
id: string
|
||||
project_id: string
|
||||
name: string
|
||||
original_crs?: string | null
|
||||
area_m2?: number | null
|
||||
created_at?: string | null
|
||||
geometry_type?: string | null
|
||||
}
|
||||
|
||||
export interface AreaCreate {
|
||||
name: string
|
||||
geometry: GeoJSON.Polygon | GeoJSON.MultiPolygon
|
||||
crs?: string
|
||||
}
|
||||
|
||||
export interface AreaListResponse {
|
||||
items: AreaRead[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface DatasetCreateResponse {
|
||||
id: string
|
||||
name: string
|
||||
dataset_type: string
|
||||
source: string
|
||||
dataset_role?: string
|
||||
source_name?: string | null
|
||||
reference_layer_name?: string | null
|
||||
source_metadata?: Record<string, unknown> | null
|
||||
provenance_metadata?: Record<string, unknown> | null
|
||||
imported_at?: string | null
|
||||
project_id: string
|
||||
area_id?: string | null
|
||||
storage_path?: string | null
|
||||
original_filename?: string | null
|
||||
stored_filename?: string | null
|
||||
content_type?: string | null
|
||||
size_bytes?: number | null
|
||||
checksum_sha256?: string | null
|
||||
crs?: string | null
|
||||
bounds_json?: Record<string, number> | null
|
||||
metadata_json?: Record<string, unknown> | null
|
||||
vector_summary?: VectorSummary | null
|
||||
status: string
|
||||
derived_from_dataset_id?: string | null
|
||||
created_at?: string | null
|
||||
feature_count?: number | null
|
||||
}
|
||||
|
||||
export interface JobRead {
|
||||
id: string
|
||||
job_type: string
|
||||
status: string
|
||||
project_id: string
|
||||
dataset_id?: string | null
|
||||
input_dataset_id?: string | null
|
||||
output_dataset_id?: string | null
|
||||
parameters_json: Record<string, unknown>
|
||||
result_json?: Record<string, unknown> | null
|
||||
error_message?: string | null
|
||||
created_at?: string | null
|
||||
started_at?: string | null
|
||||
finished_at?: string | null
|
||||
}
|
||||
|
||||
export interface JobListResponse {
|
||||
items: JobRead[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface VectorSummary {
|
||||
feature_count?: number | null
|
||||
geometry_types?: string[] | null
|
||||
bounds_json?: Record<string, number> | null
|
||||
approximate_area_m2?: number | null
|
||||
crs?: string | null
|
||||
feature_geometry_count?: number | null
|
||||
invalid_features?: number | null
|
||||
crs_assumed?: boolean | null
|
||||
}
|
||||
|
||||
export interface VectorSummaryResponse {
|
||||
feature_count?: number | null
|
||||
geometry_types?: string[] | null
|
||||
bounds_json?: Record<string, number> | null
|
||||
approximate_area_m2?: number | null
|
||||
crs?: string | null
|
||||
feature_geometry_count?: number | null
|
||||
invalid_features?: number | null
|
||||
crs_assumed?: boolean | null
|
||||
}
|
||||
|
||||
export interface RasterMetadataResponse {
|
||||
driver: string
|
||||
dataset_id?: string
|
||||
width: number
|
||||
height: number
|
||||
band_count: number
|
||||
crs?: string | null
|
||||
bounds?: number[]
|
||||
resolution?: number[]
|
||||
dtype?: string[]
|
||||
nodata?: unknown
|
||||
transform?: string[] | number[] | null
|
||||
path?: string
|
||||
size_bytes?: number | null
|
||||
checksum_sha256?: string | null
|
||||
operation?: string | null
|
||||
operation_parameters?: Record<string, unknown> | null
|
||||
source_dataset_id?: string | null
|
||||
}
|
||||
|
||||
export interface RasterInspectResponse {
|
||||
dataset_id: string
|
||||
ready: boolean
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface RasterPreviewPayload {
|
||||
path: string
|
||||
format: string
|
||||
width: number | null
|
||||
height: number | null
|
||||
}
|
||||
|
||||
export interface RasterPreviewResponse {
|
||||
dataset_id: string
|
||||
ready: boolean
|
||||
preview: RasterPreviewPayload
|
||||
metadata?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface RasterBandStats {
|
||||
band_index: number
|
||||
dtype: string | null
|
||||
min: number | null
|
||||
max: number | null
|
||||
mean: number | null
|
||||
std: number | null
|
||||
nodata_count: number
|
||||
nodata_ratio: number
|
||||
valid_pixel_count: number
|
||||
histogram: number[] | null
|
||||
histogram_bins: number[] | null
|
||||
}
|
||||
|
||||
export interface RasterStatsResponse {
|
||||
dataset_id: string
|
||||
source_dataset_id?: string | null
|
||||
bands: RasterBandStats[]
|
||||
generated_at?: string | null
|
||||
}
|
||||
|
||||
export interface RasterNdviRequest {
|
||||
nir_band: number
|
||||
red_band: number
|
||||
output_name?: string
|
||||
}
|
||||
|
||||
export interface RasterNdwiRequest {
|
||||
green_band: number
|
||||
nir_band: number
|
||||
output_name?: string
|
||||
}
|
||||
|
||||
export interface RasterNdbiRequest {
|
||||
swir_band: number
|
||||
nir_band: number
|
||||
output_name?: string
|
||||
}
|
||||
|
||||
export interface RasterTileManifestTile {
|
||||
path: string
|
||||
pixel_window: number[]
|
||||
bounds: number[]
|
||||
transform: number[]
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface RasterTileManifest {
|
||||
tile_set_id: string
|
||||
source_dataset_id: string
|
||||
source_raster_id: string
|
||||
bounds: number[]
|
||||
tile_size: number
|
||||
overlap: number
|
||||
parameters: Record<string, unknown>
|
||||
created_at: string
|
||||
tile_paths: string[]
|
||||
count: number
|
||||
tiles: RasterTileManifestTile[]
|
||||
}
|
||||
|
||||
export interface RasterTileResponse {
|
||||
dataset_id: string
|
||||
ready: boolean
|
||||
operation: string
|
||||
tile_set_id: string
|
||||
tile_size: number
|
||||
overlap: number
|
||||
manifest_path: string
|
||||
count: number
|
||||
manifest: RasterTileManifest
|
||||
}
|
||||
|
||||
export interface VectorBBoxResponse {
|
||||
dataset_id: string
|
||||
bounds_json: Record<string, number> | null
|
||||
feature_count: number
|
||||
crs?: string | null
|
||||
}
|
||||
|
||||
export interface VectorStatsResponse {
|
||||
dataset_id: string
|
||||
feature_count: number
|
||||
geometry_type_summary: Record<string, number>
|
||||
bounds_json: Record<string, number> | null
|
||||
crs?: string | null
|
||||
}
|
||||
|
||||
export interface DatasetListResponse {
|
||||
items: DatasetCreateResponse[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface GeojsonEnvelopeResponse {
|
||||
data: object
|
||||
}
|
||||
|
||||
export interface ProviderCapability {
|
||||
provider_name: string
|
||||
display_name: string
|
||||
authority_level: 'authoritative' | 'contextual' | 'manual' | 'fixture' | string
|
||||
supported_layers: string[]
|
||||
supported_geometry_types: string[]
|
||||
supported_query_modes: string[]
|
||||
fetch_signature: string
|
||||
configured: boolean
|
||||
status: string
|
||||
limitation_message: string
|
||||
attribution: string
|
||||
license_note: string
|
||||
not_configured_reason: string | null
|
||||
}
|
||||
|
||||
export interface SystemCapabilitiesResponse {
|
||||
status: string
|
||||
service: string
|
||||
version: string
|
||||
database: string | null
|
||||
postgis: boolean
|
||||
rasterio: boolean
|
||||
geopandas: boolean
|
||||
yolo: boolean | string
|
||||
sam: boolean | string
|
||||
grb: string
|
||||
sentinel: string
|
||||
providers: ProviderCapability[]
|
||||
}
|
||||
|
||||
export interface ProviderCapabilitiesResponse {
|
||||
providers: ProviderCapability[]
|
||||
}
|
||||
|
||||
export interface ProviderLayersResponse {
|
||||
provider_name: string
|
||||
layers: string[]
|
||||
}
|
||||
|
||||
export interface ProviderStatusResponse {
|
||||
provider_name: string
|
||||
configured: boolean
|
||||
status: string
|
||||
limitation_message: string
|
||||
}
|
||||
|
||||
export interface ProviderImportResponse {
|
||||
provider_name: string
|
||||
status: string
|
||||
message: string
|
||||
requested_layers: string[]
|
||||
dataset_id: string | null
|
||||
dataset_role?: string | null
|
||||
source_name?: string | null
|
||||
}
|
||||
|
||||
export interface DetectionModelCapability {
|
||||
model_id: string
|
||||
display_name: string
|
||||
framework: string
|
||||
task_type: string
|
||||
supported_classes: string[]
|
||||
configured: boolean
|
||||
status: string
|
||||
limitation_message: string
|
||||
version?: string | null
|
||||
}
|
||||
|
||||
export interface DetectionModelsResponse {
|
||||
models: DetectionModelCapability[]
|
||||
}
|
||||
|
||||
export interface DetectionRunRequest {
|
||||
project_id: string
|
||||
dataset_id: string
|
||||
model_id: string
|
||||
confidence_threshold: number
|
||||
class_filter?: string[] | null
|
||||
tile_manifest_path?: string | null
|
||||
parameters_json?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface DetectionRunResponse {
|
||||
analysis_run_id: string
|
||||
job_id: string
|
||||
project_id: string
|
||||
dataset_id: string
|
||||
model_id: string
|
||||
status: string
|
||||
detection_count: number
|
||||
error_code?: string | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface DetectionRunRead {
|
||||
id: string
|
||||
project_id: string
|
||||
dataset_id?: string | null
|
||||
job_id?: string | null
|
||||
analysis_type: string
|
||||
status: string
|
||||
model_name?: string | null
|
||||
model_version?: string | null
|
||||
parameters_json: Record<string, unknown>
|
||||
result_json?: Record<string, unknown> | null
|
||||
error_message?: string | null
|
||||
created_at?: string | null
|
||||
started_at?: string | null
|
||||
finished_at?: string | null
|
||||
}
|
||||
|
||||
export interface DetectionRunListResponse {
|
||||
items: DetectionRunRead[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface DetectionRead {
|
||||
id: string
|
||||
project_id: string
|
||||
dataset_id?: string | null
|
||||
analysis_run_id?: string | null
|
||||
job_id?: string | null
|
||||
model_name: string
|
||||
model_version?: string | null
|
||||
class_name: string
|
||||
confidence: number
|
||||
bbox_json?: Record<string, unknown> | null
|
||||
source_tile_path?: string | null
|
||||
properties_json?: Record<string, unknown> | null
|
||||
created_at?: string | null
|
||||
}
|
||||
|
||||
export interface DetectionListResponse {
|
||||
items: DetectionRead[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface DetectionQaRequest {
|
||||
reference_dataset_id: string
|
||||
iou_threshold: number
|
||||
class_name?: string | null
|
||||
min_confidence?: number | null
|
||||
}
|
||||
|
||||
export interface DetectionQaResult {
|
||||
status: string
|
||||
quality_check_id: string
|
||||
analysis_run_id: string
|
||||
reference_dataset_id: string
|
||||
candidate_feature_count: number
|
||||
reference_feature_count: number
|
||||
matches: number
|
||||
false_positives: number
|
||||
false_negatives: number
|
||||
precision?: number | null
|
||||
recall?: number | null
|
||||
f1_score?: number | null
|
||||
mean_iou?: number | null
|
||||
iou_threshold: number
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
export type SegmentationModelCapability = DetectionModelCapability
|
||||
|
||||
export interface SegmentationModelsResponse {
|
||||
models: SegmentationModelCapability[]
|
||||
}
|
||||
|
||||
export interface SegmentationRunRequest {
|
||||
project_id: string
|
||||
dataset_id: string
|
||||
model_id: string
|
||||
confidence_threshold: number
|
||||
class_filter?: string[] | null
|
||||
tile_manifest_path?: string | null
|
||||
parameters_json?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface SegmentationRunResponse {
|
||||
analysis_run_id: string
|
||||
job_id: string
|
||||
project_id: string
|
||||
dataset_id: string
|
||||
model_id: string
|
||||
status: string
|
||||
segmentation_count: number
|
||||
error_code?: string | null
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface SegmentationRunRead {
|
||||
id: string
|
||||
project_id: string
|
||||
dataset_id?: string | null
|
||||
job_id?: string | null
|
||||
analysis_type: string
|
||||
status: string
|
||||
model_name?: string | null
|
||||
model_version?: string | null
|
||||
parameters_json: Record<string, unknown>
|
||||
result_json?: Record<string, unknown> | null
|
||||
error_message?: string | null
|
||||
created_at?: string | null
|
||||
started_at?: string | null
|
||||
finished_at?: string | null
|
||||
}
|
||||
|
||||
export interface SegmentationRunListResponse {
|
||||
items: SegmentationRunRead[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface SegmentationRead {
|
||||
id: string
|
||||
project_id: string
|
||||
dataset_id?: string | null
|
||||
analysis_run_id?: string | null
|
||||
job_id?: string | null
|
||||
model_name: string
|
||||
model_version?: string | null
|
||||
class_name: string
|
||||
confidence?: number | null
|
||||
bbox_json?: Record<string, unknown> | null
|
||||
area_m2?: number | null
|
||||
mask_path?: string | null
|
||||
source_tile_path?: string | null
|
||||
tile_index?: number | null
|
||||
properties_json?: Record<string, unknown> | null
|
||||
provenance_json?: Record<string, unknown> | null
|
||||
created_at?: string | null
|
||||
}
|
||||
|
||||
export interface SegmentationListResponse {
|
||||
items: SegmentationRead[]
|
||||
total: number
|
||||
}
|
||||
|
||||
export interface SegmentationQaRequest {
|
||||
reference_dataset_id: string
|
||||
iou_threshold: number
|
||||
class_name?: string | null
|
||||
min_confidence?: number | null
|
||||
}
|
||||
|
||||
export type SegmentationQaResult = DetectionQaResult
|
||||
|
||||
export interface QaComparisonRequest {
|
||||
candidate_dataset_id: string
|
||||
reference_dataset_id: string
|
||||
iou_threshold: number
|
||||
area_id?: string | null
|
||||
}
|
||||
|
||||
export interface QaComparisonResult {
|
||||
status: string
|
||||
warnings: string[]
|
||||
candidate_feature_count: number
|
||||
reference_feature_count: number
|
||||
matches: number
|
||||
false_positives: number
|
||||
false_negatives: number
|
||||
precision: number | null
|
||||
recall: number | null
|
||||
f1_score: number | null
|
||||
mean_iou: number | null
|
||||
iou_threshold: number
|
||||
unsupported_geometry: boolean
|
||||
unsupported_geometries: string[]
|
||||
generated_at: string
|
||||
}
|
||||
|
||||
export interface MetricRead {
|
||||
id: string
|
||||
quality_check_id?: string | null
|
||||
analysis_run_id?: string | null
|
||||
metric_key: string
|
||||
metric_value?: number | null
|
||||
metric_unit?: string | null
|
||||
label?: string | null
|
||||
metadata_json?: Record<string, unknown> | null
|
||||
created_at?: string | null
|
||||
}
|
||||
|
||||
export interface QualityCheckRead {
|
||||
id: string
|
||||
project_id: string
|
||||
job_id?: string | null
|
||||
analysis_run_id?: string | null
|
||||
candidate_dataset_id?: string | null
|
||||
reference_dataset_id: string
|
||||
check_type: string
|
||||
status: string
|
||||
score?: number | null
|
||||
parameters_json?: Record<string, unknown> | null
|
||||
findings_json?: Record<string, unknown> | null
|
||||
created_at?: string | null
|
||||
completed_at?: string | null
|
||||
metrics: MetricRead[]
|
||||
}
|
||||
|
||||
export interface QualityCheckListResponse {
|
||||
items: QualityCheckRead[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export type ExportKind = 'dataset' | 'detection_run' | 'segmentation_run'
|
||||
|
||||
export interface ExportRead {
|
||||
id: string
|
||||
project_id: string
|
||||
analysis_run_id?: string | null
|
||||
export_type: string
|
||||
storage_path: string
|
||||
metadata_json?: Record<string, unknown> | null
|
||||
created_at?: string | null
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ExportCreateResponse {
|
||||
export_id: string
|
||||
path: string
|
||||
status: string
|
||||
export_type: string
|
||||
metadata_json?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface ExportListResponse {
|
||||
items: ExportRead[]
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface ExportContentResponse {
|
||||
export_id: string
|
||||
export_type: string
|
||||
content: Record<string, unknown>
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2022"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": false,
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "src/**/*.tsx", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
declare const process: { env: Record<string, string | undefined> }
|
||||
|
||||
const apiProxyTarget = process.env.VITE_API_PROXY_TARGET ?? 'http://localhost:8000'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
chunkSizeWarningLimit: 900,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
maplibre: ['maplibre-gl'],
|
||||
vendor: ['react', 'react-dom'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: apiProxyTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/health': {
|
||||
target: apiProxyTarget,
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user