Refine premium workbench UX
This commit is contained in:
@@ -7,6 +7,16 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## Sprint 180 Premium workbench UX hardening (2026-07-14)
|
||||
|
||||
- Rebuilt the workbench presentation hierarchy around grouped task navigation, a compact Mol context header and an optional selection-detail drawer instead of a permanent third column.
|
||||
- Fixed the narrow-screen shell so navigation is horizontal and the active workspace receives the full viewport width; desktop and ultrawide content now use stable, centered work areas.
|
||||
- Reworked Overview into one operational status band, one workflow rail and compact quick actions without changing readiness calculations or routing.
|
||||
- Made Data operational with three bounded project/AOI/dataset columns, scroll-safe populated catalogs and progressively disclosed create/upload forms.
|
||||
- Made Map controls and MapLibre the primary surface while collapsing provenance, BBox inputs and raw feature inspection into explicit detail disclosures.
|
||||
- Made AI Labs task-first by placing detection/segmentation run controls before model-registry diagnostics and collapsing registry/preflight detail.
|
||||
- Added focused UI regression coverage; no API contract, migration, persistence, GIS operation, QA metric, model dependency or inference behavior changed.
|
||||
|
||||
## Sprint 179 Mol detection evidence diagnosis (2026-07-13)
|
||||
|
||||
- Added a read-only, storage-confined false-negative contact-sheet renderer that projects persisted missed GRB geometries onto the exact source tile manifest recorded by the analysis run.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def read(path: str) -> str:
|
||||
return (ROOT / path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_workbench_uses_grouped_navigation_and_optional_inspector() -> None:
|
||||
app = read("frontend/src/App.tsx")
|
||||
inspector = read("frontend/src/components/inspector/WorkbenchInspector.tsx")
|
||||
|
||||
assert "import './styles/premium.css'" in app
|
||||
assert "const workspaceNavGroups" in app
|
||||
assert "['overview', 'data', 'map']" in app
|
||||
assert "['analysis', 'ai']" in app
|
||||
assert "const [inspectorOpen, setInspectorOpen] = useState(false)" in app
|
||||
assert "{inspectorOpen ? (" in app
|
||||
assert 'id="workbench-inspector"' in app
|
||||
assert "onClose={() => setInspectorOpen(false)}" in app
|
||||
assert 'className="inspector-close"' in inspector
|
||||
|
||||
|
||||
def test_data_creation_forms_are_progressively_disclosed() -> None:
|
||||
project = read("frontend/src/components/project/ProjectPanel.tsx")
|
||||
area = read("frontend/src/components/project/AreaPanel.tsx")
|
||||
dataset = read("frontend/src/components/datasets/DatasetPanel.tsx")
|
||||
css = read("frontend/src/styles/premium.css")
|
||||
|
||||
assert '<details className="data-panel-form-block">' in project
|
||||
assert '<summary>Create project</summary>' in project
|
||||
assert '<details className="data-panel-form-block">' in area
|
||||
assert '<summary>Create spatial scope</summary>' in area
|
||||
assert '<details className="dataset-upload-block data-panel-form-block">' in dataset
|
||||
assert '<summary>Upload source data</summary>' in dataset
|
||||
assert "details.data-panel-form-block > summary" in css
|
||||
assert ".workspace-grid-data > section:nth-child(n)" in css
|
||||
assert "max-height: calc(100dvh - 10.5rem);" in css
|
||||
|
||||
|
||||
def test_map_prioritizes_controls_map_and_collapsed_diagnostics() -> None:
|
||||
map_workspace = read("frontend/src/components/map/MapWorkspace.tsx")
|
||||
css = read("frontend/src/styles/premium.css")
|
||||
|
||||
control_index = map_workspace.index('className="map-control-surface"')
|
||||
map_index = map_workspace.index('className="map-frame-surface"')
|
||||
detail_index = map_workspace.index('className="map-layer-details"')
|
||||
assert control_index < map_index < detail_index
|
||||
assert '<details className="map-layer-details">' in map_workspace
|
||||
assert '<details className="map-advanced-tools">' in map_workspace
|
||||
assert 'className="map-advanced-tools-body"' in map_workspace
|
||||
assert ".map-control-surface {\n order: 1;" in css
|
||||
assert ".map-frame-surface {\n order: 2;" in css
|
||||
assert ".map-layer-details {\n order: 3;" in css
|
||||
|
||||
|
||||
def test_ai_workspaces_prioritize_runs_and_collapse_registry_detail() -> None:
|
||||
detection = read("frontend/src/components/detection/DetectionLab.tsx")
|
||||
segmentation = read("frontend/src/components/segmentation/SegmentationLab.tsx")
|
||||
css = read("frontend/src/styles/premium.css")
|
||||
|
||||
assert '<details className="ai-lab-model-surface" aria-label="Detection model capabilities">' in detection
|
||||
assert '<details className="ai-lab-model-surface" aria-label="YOLO runtime preflight">' in detection
|
||||
assert '<details className="ai-lab-model-surface" aria-label="Segmentation model capabilities">' in segmentation
|
||||
assert ".ai-lab-shell > .lab-block {\n order: 2;" in css
|
||||
assert ".ai-lab-shell > .ai-lab-model-surface {\n order: 7;" in css
|
||||
assert "details.ai-lab-model-surface > summary" in css
|
||||
|
||||
|
||||
def test_mobile_shell_uses_full_width_main_and_horizontal_navigation() -> None:
|
||||
css = read("frontend/src/styles/premium.css")
|
||||
|
||||
assert "@media (max-width: 920px)" in css
|
||||
assert ".app-shell > header.workbench-topbar" in css
|
||||
assert ".workbench-layout {\n display: block;" in css
|
||||
assert ".workbench-sidebar nav {\n display: flex;" in css
|
||||
assert ".nav-group {\n display: contents;" in css
|
||||
assert ".workbench-inspector {\n top: 0;\n width: 100vw;" in css
|
||||
assert "grid-auto-flow: column;" in css
|
||||
assert "overflow-x: auto;" in css
|
||||
@@ -7345,3 +7345,42 @@ Open:
|
||||
- Browser verification on `http://192.168.10.150:1202` selected the complete Mol Postel workspace and showed ready project/AOI/dataset/QA/export state.
|
||||
- The Map workspace rendered one `1033x542` MapLibre canvas, the OSM road basemap, AOI and `95` persisted detections with no browser warnings/errors, no visible dialog and no horizontal document overflow at a `1280 px` viewport.
|
||||
- No API contract, migration, QualityCheck/Metric row, Detection row, model asset, active-model configuration or inference result changed in this pass.
|
||||
|
||||
# Sprint 180 - Premium workbench UX hardening
|
||||
|
||||
## Implementation
|
||||
|
||||
- Reworked the workbench shell into a calmer operational hierarchy with grouped Workspace, Analyze and Deliver navigation, a compact Mol/Kempen context header and a centered content canvas.
|
||||
- Replaced the permanently reserved inspector column with an on-demand detail drawer. Dataset inspection still invokes the existing detail-loading callback and now opens the drawer explicitly.
|
||||
- Made Data operational at scale by disclosing create/upload forms on demand, arranging Project, AOI and Dataset panels side by side on desktop and bounding their long collections with internal scrolling.
|
||||
- Promoted MapLibre to the primary Map surface. Layer provenance and low-frequency BBox/raw-feature controls remain available in collapsed detail surfaces without removing any GIS action.
|
||||
- Reordered Detection and Segmentation Labs around run controls and result review; model registry and YOLO preflight remain fully available as secondary disclosures.
|
||||
- Added a dedicated `premium.css` presentation layer with a restrained neutral/teal palette, consistent controls, stable panel dimensions and an actual full-width mobile shell. The former narrow sidebar-plus-content split is removed below `920 px`.
|
||||
- Added focused static regression coverage for navigation grouping, optional inspector wiring, scalable Data panels, map hierarchy, AI Lab ordering and responsive behavior.
|
||||
|
||||
## Behavior preservation
|
||||
|
||||
- Existing React state hooks, API clients, service calls, map callbacks, QA actions, uploads, exports and AI run handlers were retained.
|
||||
- No backend application code, endpoint contract, database model, migration, geospatial algorithm, model configuration or persisted evidence was changed.
|
||||
- Legacy UI contract strings and callback signatures remain present for the existing Sprint 1-179 regression suite.
|
||||
|
||||
## Visual audit
|
||||
|
||||
- At `1280x720`, the Overview now uses one compact readiness row and no permanent inspector reservation; Map controls, canvas and secondary layer details follow the intended visual order.
|
||||
- At `2560x1080`, the main workbench uses a centered `1680 px` maximum content width while the map can expand independently; the previous permanently empty `416 px` inspector column is gone.
|
||||
- At `390x844`, navigation becomes a horizontal full-width rail and the main workspace occupies the viewport instead of sharing it with a `152 px` sidebar.
|
||||
- A live 50-project Data state now keeps project and dataset panels within the viewport with internal scrolling instead of pushing the Dataset panel thousands of pixels below the fold.
|
||||
- Detection and Segmentation run controls appear before registry diagnostics, reducing the distance to the primary task while retaining honest model-state detail.
|
||||
|
||||
## Local validation
|
||||
|
||||
- React best-practice review found no new conditional hooks, effect synchronization, unstable list keys or non-semantic interactive controls.
|
||||
- Focused premium-workbench and legacy UI regression coverage passed.
|
||||
- `python -m compileall backend/app`: passed.
|
||||
- Full backend suite: `492 passed`.
|
||||
- Frontend typecheck and production build: passed; app bundle `219.76 kB`, React vendor `140.74 kB` and MapLibre `801.82 kB` before gzip.
|
||||
- `bash scripts/run_readiness_check.sh`: passed with the 81-route API contract audit, single Alembic head, all backend tests, frontend typecheck/build and live-smoke syntax gate.
|
||||
|
||||
## Next pass
|
||||
|
||||
- Harden detection QA coverage and matching diagnostics before making a retraining decision; keep that work separate from this presentation-only sprint.
|
||||
|
||||
@@ -402,6 +402,14 @@ This file now starts with the current implementation status. Older preparation/b
|
||||
|
||||
# Current Workbench UI
|
||||
|
||||
- [x] Replace the accumulated shell overrides with a coherent premium presentation layer while preserving V1 behavior.
|
||||
- [x] Group navigation into Workspace, Analyze and Deliver and make the inspector an optional drawer.
|
||||
- [x] Fix mobile navigation so the active workspace uses the full viewport width.
|
||||
- [x] Bound populated Project, Dataset, QA, AI and Export panels so long histories do not push core actions thousands of pixels down-page.
|
||||
- [x] Move Data create/upload forms and Map/AI diagnostics into explicit progressive disclosures.
|
||||
- [x] Reorder AI Labs around run controls and Map around layer selection plus the MapLibre canvas.
|
||||
- [ ] Harden detection QA coverage and matching diagnostics before any further model training.
|
||||
|
||||
- [x] Replace the one-page workflow panel stack with a task-based workbench shell.
|
||||
- [x] Add persistent project/AOI/dataset/layer context.
|
||||
- [x] Move selected dataset details into a persistent inspector.
|
||||
|
||||
+8
-7
@@ -4,13 +4,13 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
|
||||
|
||||
Mol is the primary operating context. On a fresh session the workbench prefers a persisted Mol project or dataset, centers an empty MapLibre view on Mol and pre-fills a compact Mol AOI. Explicit project and dataset selections remain authoritative, and all broader Kempen workflows remain available.
|
||||
|
||||
The workbench now uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and right-side dataset inspector.
|
||||
The workbench uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and an optional selection-detail drawer.
|
||||
|
||||
The shell has a calmer V1 workbench density pass: the duplicated workspace command bar is hidden, the sidebar is more compact, the top context bar is quieter, readiness tiles are smaller and panels use softer borders/shadows. This keeps the same workflows and API calls while reducing visual noise on desktop and mobile.
|
||||
The premium V1 presentation layer lives in `src/styles/premium.css`. It groups navigation by Workspace, Analyze and Deliver, removes the permanent inspector column, gives desktop/ultrawide workspaces stable readable widths and switches narrow screens to full-width content with a horizontally scrollable navigation rail. API calls and workflow state remain owned by the existing hooks.
|
||||
|
||||
Data and Map have an additional usability layout pass for the core daily workflow. The Data workspace uses compact catalog cards, shorter action buttons and tighter role summaries. The Map workspace prioritizes the MapLibre frame before dense controls, gives the map more height on desktop and compresses context/provenance/bbox extraction surfaces while preserving the existing selection, export and QA actions.
|
||||
Data and Map are organized around the core daily workflow. Data shows Project, AOI and Dataset columns together on normal desktop widths, bounds populated lists inside their own panels and keeps create/upload forms in explicit disclosures. Map keeps the layer/AOI command surface and MapLibre frame first, then exposes provenance, BBox controls and raw feature inspection only when requested. Existing selection, export and QA actions are unchanged.
|
||||
|
||||
Wide and ultrawide screens now get dedicated workbench breakpoints at `1800px` and `2200px`. These keep the sidebar and inspector readable, expand the MapLibre review frame and use extra horizontal space for Data, Analysis, AI and Export grids without changing API calls or workflows.
|
||||
Wide and ultrawide screens keep a readable sidebar and centered work area, expand the MapLibre review frame and use extra horizontal space for Data, Analysis, AI and Export grids. The detail drawer overlays the work area only while open, so it does not permanently consume ultrawide canvas space.
|
||||
|
||||
The Map workspace defaults to an OpenStreetMap road basemap with visible attribution so uploaded vectors, AOIs and QA overlays appear on a real street context. Set `VITE_MAP_STYLE_URL` to a managed MapLibre style URL to override this for production or high-volume deployments.
|
||||
|
||||
@@ -288,7 +288,7 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
||||
- The Overview workspace now also shows a compact workflow guidance rail for the V1 path from project and AOI setup through data, map review, QA/AI validation and export handoff.
|
||||
- When the V1 workflow is populated end to end, the Overview rail switches to a `Ready for handoff` state and keeps map guidance explicit about layer features versus AOI context.
|
||||
- Overview rail Map and Export clicks reuse the first ready vector/GeoJSON dataset when no layer or dataset is active, keeping the happy path connected without adding new API calls.
|
||||
- The Data workspace surfaces selected project, AOI and dataset context before creation/upload forms, then separates form and catalog/list regions for faster scanning.
|
||||
- The Data workspace surfaces selected project, AOI and dataset context first, keeps creation/upload forms in closed-by-default disclosures and constrains large populated catalogs to independently scrollable panels.
|
||||
- The Export Center includes a handoff readiness summary, grouped artifact actions and provenance-rich export cards so report/GeoJSON handoff stays understandable in long-running demo projects.
|
||||
- The Export Center highlights the latest project report, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON artifacts with direct preview/download actions.
|
||||
- The offline demo workflow now selects the seeded raster fixture for Detection and Segmentation Lab prerequisites while keeping the candidate vector dataset open for Data/Map/Export review.
|
||||
@@ -300,10 +300,11 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
||||
- `workbench-topbar`: active project, AOI, dataset and layer context.
|
||||
- `workbench-sidebar`: primary navigation between workflow workspaces.
|
||||
- `workbench-main`: one active workflow at a time.
|
||||
- `workbench-inspector`: persistent selected dataset details and raster/vector operation controls.
|
||||
- `workbench-inspector`: optional selected dataset details and raster/vector operation controls, opened from the active workspace heading.
|
||||
- Existing API calls, hooks, MapLibre rendering and QA/AI/export flows are unchanged.
|
||||
- Stable navigation test anchors use `data-testid="workspace-nav-{workspace}"`.
|
||||
- Mobile workbench navigation uses horizontal rails for the primary nav and command bar, avoiding a tall menu stack before the active workspace content.
|
||||
- Mobile workbench navigation uses a full-width horizontal primary rail, avoiding the former fixed 152 px sidebar that compressed the active workspace.
|
||||
- Detection and Segmentation Labs present run controls first; model registry and YOLO preflight diagnostics remain available as collapsed detail surfaces below the operational workflow.
|
||||
- The Map workspace shows active layer source/provenance/draw-state context and selected-feature property chips before the raw JSON inspector.
|
||||
- When the Map workspace has no active result layer, it lists ready vector/GeoJSON datasets as direct quick actions so populated demo projects can jump straight from the empty state to map inspection.
|
||||
- The Map workspace can extract persisted vector features by area: open a ready vector dataset, use `Start map bbox` and click two map corners or enter EPSG:4326 bbox values, then run `Run area extract` to query backend `vector_features`. Results are highlighted on the map and can be downloaded as GeoJSON.
|
||||
|
||||
+64
-28
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import './styles/app.css'
|
||||
import './styles/premium.css'
|
||||
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
|
||||
import { DatasetPanel } from './components/datasets/DatasetPanel'
|
||||
import { DetectionLab } from './components/detection/DetectionLab'
|
||||
@@ -45,10 +46,18 @@ const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description:
|
||||
{ key: 'system', label: 'System', description: 'Provider capabilities' },
|
||||
]
|
||||
|
||||
const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [
|
||||
{ label: 'Workspace', keys: ['overview', 'data', 'map'] },
|
||||
{ label: 'Analyze', keys: ['analysis', 'ai'] },
|
||||
{ label: 'Deliver', keys: ['exports', 'system'] },
|
||||
]
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')
|
||||
const [inspectorOpen, setInspectorOpen] = useState(false)
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, left: 0 })
|
||||
setInspectorOpen(false)
|
||||
}, [activeWorkspace])
|
||||
|
||||
const {
|
||||
@@ -643,8 +652,11 @@ function App(): JSX.Element {
|
||||
</a>
|
||||
<header className="workbench-topbar">
|
||||
<div className="brand-block">
|
||||
<p className="eyebrow">GeoAI Workbench · Mol / Kempen</p>
|
||||
<h1>GeoIntel Kempen</h1>
|
||||
<span className="brand-mark" aria-hidden="true">GI</span>
|
||||
<div>
|
||||
<h1>GeoIntel</h1>
|
||||
<p>GeoAI Workbench · Mol / Kempen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="context-bar" aria-label="Active workbench context">
|
||||
<div>
|
||||
@@ -671,30 +683,54 @@ function App(): JSX.Element {
|
||||
<div className="workbench-layout">
|
||||
<aside className="workbench-sidebar" aria-label="Workbench navigation">
|
||||
<nav aria-label="Primary workspaces">
|
||||
{workspaceNavItems.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'}
|
||||
onClick={() => setActiveWorkspace(item.key)}
|
||||
aria-current={item.key === activeWorkspace ? 'page' : undefined}
|
||||
aria-label={`Open ${item.label} workspace: ${item.description}`}
|
||||
data-testid={`workspace-nav-${item.key}`}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<small>{item.description}</small>
|
||||
</button>
|
||||
{workspaceNavGroups.map((group) => (
|
||||
<div className="nav-group" key={group.label}>
|
||||
<p className="nav-section-label">{group.label}</p>
|
||||
{group.keys.map((key) => {
|
||||
const item = workspaceNavItems.find((candidate) => candidate.key === key)
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<button
|
||||
key={item.key}
|
||||
type="button"
|
||||
className={item.key === activeWorkspace ? 'nav-item nav-item-active' : 'nav-item'}
|
||||
onClick={() => setActiveWorkspace(item.key)}
|
||||
aria-current={item.key === activeWorkspace ? 'page' : undefined}
|
||||
aria-label={`Open ${item.label} workspace: ${item.description}`}
|
||||
data-testid={`workspace-nav-${item.key}`}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<small>{item.description}</small>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<main className="workbench-main" id="workspace-main" tabIndex={-1}>
|
||||
<div className="workspace-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Workspace</p>
|
||||
<div className="workspace-heading-copy">
|
||||
<p className="eyebrow">{selectedProject?.region ?? 'Mol, Kempen'}</p>
|
||||
<h2>{activeWorkspaceItem.label}</h2>
|
||||
</div>
|
||||
<p>{activeWorkspaceItem.description}</p>
|
||||
<div className="workspace-heading-actions">
|
||||
<p>{activeWorkspaceItem.description}</p>
|
||||
{activeWorkspace !== 'overview' ? (
|
||||
<button
|
||||
type="button"
|
||||
className={inspectorOpen ? 'inspector-toggle inspector-toggle-active' : 'inspector-toggle'}
|
||||
onClick={() => setInspectorOpen((current) => !current)}
|
||||
aria-expanded={inspectorOpen}
|
||||
aria-controls="workbench-inspector"
|
||||
>
|
||||
{inspectorOpen ? 'Close details' : 'Open details'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="workspace-command-bar" aria-label="Workspace shortcuts">
|
||||
<div className="workspace-nav-cluster">
|
||||
@@ -763,12 +799,8 @@ function App(): JSX.Element {
|
||||
</section>
|
||||
<section className="overview-actions">
|
||||
<div className="overview-action-copy">
|
||||
<p className="eyebrow">Recommended flow</p>
|
||||
<h2>Build the project, load data, review on map, validate, export.</h2>
|
||||
<p className="muted">
|
||||
Keep the daily workflow focused: prepare data first, inspect it spatially, then run QA/QC or AI lab tasks only when the
|
||||
required datasets are present.
|
||||
</p>
|
||||
<p className="eyebrow">Quick access</p>
|
||||
<h2>Continue working</h2>
|
||||
</div>
|
||||
<div className="quick-action-grid overview-quick-actions" aria-label="Recommended next workbench actions">
|
||||
<button
|
||||
@@ -845,6 +877,7 @@ function App(): JSX.Element {
|
||||
onDatasetFormChange={setDatasetForm}
|
||||
onUploadDataset={uploadDataset}
|
||||
onLoadDatasetDetails={loadDatasetDetails}
|
||||
onOpenInspector={() => setInspectorOpen(true)}
|
||||
onRefreshMetadata={refreshMetadata}
|
||||
onOpenDatasetInMap={openDatasetInMap}
|
||||
onOpenDatasetExport={openDatasetExport}
|
||||
@@ -1080,8 +1113,9 @@ function App(): JSX.Element {
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<aside className="workbench-inspector" aria-label="Current selection inspector">
|
||||
<WorkbenchInspector
|
||||
{inspectorOpen ? (
|
||||
<aside className="workbench-inspector" id="workbench-inspector" aria-label="Current selection inspector">
|
||||
<WorkbenchInspector
|
||||
selectedProject={selectedProject}
|
||||
selectedArea={selectedArea}
|
||||
selectedMapFeature={selectedMapFeature}
|
||||
@@ -1101,6 +1135,7 @@ function App(): JSX.Element {
|
||||
onOpenQualityWorkspace={() => setActiveWorkspace('analysis')}
|
||||
onOpenExportsWorkspace={() => setActiveWorkspace('exports')}
|
||||
onOpenAiWorkspace={() => setActiveWorkspace('ai')}
|
||||
onClose={() => setInspectorOpen(false)}
|
||||
datasetDetailProps={{
|
||||
areas,
|
||||
availableVectorTargets,
|
||||
@@ -1160,8 +1195,9 @@ function App(): JSX.Element {
|
||||
onRunVectorIntersect: () => runVectorIntersect(availableVectorTargets),
|
||||
onPickDerivedDataset: pickDerivedDataset,
|
||||
}}
|
||||
/>
|
||||
</aside>
|
||||
/>
|
||||
</aside>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -105,15 +105,23 @@ export function WorkbenchStatusStrip({
|
||||
state: exports.length > 0 ? 'ready' : 'waiting',
|
||||
},
|
||||
]
|
||||
const readyCount = items.filter((item) => item.state === 'ready').length
|
||||
|
||||
return (
|
||||
<section className="workbench-status-strip" aria-label="Workbench readiness">
|
||||
<div className="status-strip-header">
|
||||
<div>
|
||||
<p className="eyebrow">V1 readiness</p>
|
||||
<h2>Workbench status</h2>
|
||||
<h2>
|
||||
<span className="sr-only">Workbench status: </span>
|
||||
{readyCount === items.length ? 'Mol workbench is ready' : 'Mol workbench needs attention'}
|
||||
</h2>
|
||||
<p className="status-next-action">{nextAction(items)}</p>
|
||||
</div>
|
||||
<div className="status-readiness" aria-label={`${readyCount} of ${items.length} workbench stages ready`}>
|
||||
<strong>{readyCount}/{items.length}</strong>
|
||||
<span>stages ready</span>
|
||||
</div>
|
||||
<p className="status-next-action">{nextAction(items)}</p>
|
||||
</div>
|
||||
<div className="status-strip-grid">
|
||||
{items.map((item) => (
|
||||
|
||||
@@ -23,6 +23,7 @@ interface DatasetPanelProps {
|
||||
onDatasetFormChange: Dispatch<SetStateAction<DatasetFormState>>
|
||||
onUploadDataset: (event: FormEvent) => void
|
||||
onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void
|
||||
onOpenInspector: () => void
|
||||
onRefreshMetadata: (datasetId: string) => void
|
||||
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
|
||||
onOpenDatasetExport: (dataset: DatasetCreateResponse) => void
|
||||
@@ -100,6 +101,7 @@ export function DatasetPanel({
|
||||
onDatasetFormChange,
|
||||
onUploadDataset,
|
||||
onLoadDatasetDetails,
|
||||
onOpenInspector,
|
||||
onRefreshMetadata,
|
||||
onOpenDatasetInMap,
|
||||
onOpenDatasetExport,
|
||||
@@ -153,8 +155,8 @@ export function DatasetPanel({
|
||||
<small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}</small>
|
||||
</div>
|
||||
|
||||
<div className="dataset-upload-block data-panel-form-block">
|
||||
<p className="data-section-label">Upload source data</p>
|
||||
<details className="dataset-upload-block data-panel-form-block">
|
||||
<summary>Upload source data</summary>
|
||||
<form className="dataset-upload-form" onSubmit={onUploadDataset}>
|
||||
<label>
|
||||
Type
|
||||
@@ -201,7 +203,7 @@ export function DatasetPanel({
|
||||
Upload dataset
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="dataset-role-summary-grid" aria-label="Dataset role summary">
|
||||
{roleSummaries.map((summary) => (
|
||||
@@ -255,6 +257,7 @@ export function DatasetPanel({
|
||||
<button
|
||||
className="primary-action dataset-action-button"
|
||||
type="button"
|
||||
onClickCapture={onOpenInspector}
|
||||
onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}
|
||||
data-testid={`dataset-select-${dataset.id}`}
|
||||
>
|
||||
|
||||
@@ -206,13 +206,12 @@ export function DetectionLab({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ai-lab-model-surface" aria-label="Detection model capabilities">
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Model registry</h3>
|
||||
<p>Backend-reported detector states and limitations.</p>
|
||||
</div>
|
||||
</div>
|
||||
<details className="ai-lab-model-surface" aria-label="Detection model capabilities">
|
||||
<summary>
|
||||
<span>Model registry</span>
|
||||
<strong>{detectionModels.length} models</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-state-stack">
|
||||
{loadingDetectionModels ? (
|
||||
<div className="result-state result-state-loading">
|
||||
@@ -254,7 +253,8 @@ export function DetectionLab({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<div className="ai-lab-model-surface" aria-label="Local model asset selection">
|
||||
@@ -359,7 +359,12 @@ export function DetectionLab({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
|
||||
<details className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
|
||||
<summary>
|
||||
<span>YOLO runtime preflight</span>
|
||||
<strong>{yoloPreflight?.status ?? 'not loaded'}</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>YOLO runtime preflight</h3>
|
||||
@@ -437,7 +442,8 @@ export function DetectionLab({
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="lab-block">
|
||||
<div className="ai-lab-run-surface" aria-label="Detection run controls">
|
||||
|
||||
@@ -35,6 +35,7 @@ interface WorkbenchInspectorProps {
|
||||
onOpenQualityWorkspace: () => void
|
||||
onOpenExportsWorkspace: () => void
|
||||
onOpenAiWorkspace: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function formatValue(value: string | number | null | undefined): string {
|
||||
@@ -74,6 +75,7 @@ export function WorkbenchInspector({
|
||||
onOpenQualityWorkspace,
|
||||
onOpenExportsWorkspace,
|
||||
onOpenAiWorkspace,
|
||||
onClose,
|
||||
}: WorkbenchInspectorProps): JSX.Element {
|
||||
const [activeTab, setActiveTab] = useState<InspectorTab>('context')
|
||||
const selectedDetectionRun = useMemo(
|
||||
@@ -103,6 +105,9 @@ export function WorkbenchInspector({
|
||||
<p className="eyebrow">Inspector</p>
|
||||
<h2>Selection details</h2>
|
||||
</div>
|
||||
<button type="button" className="inspector-close" onClick={onClose} aria-label="Close selection details">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div className="inspector-tabs" role="tablist" aria-label="Inspector detail tabs">
|
||||
{tabs.map((tab) => (
|
||||
|
||||
@@ -489,29 +489,6 @@ export function MapWorkspace({
|
||||
<span className="count-pill">{mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}</span>
|
||||
</div>
|
||||
|
||||
<div className="map-context-summary" aria-label="Map layer status">
|
||||
<div>
|
||||
<span>Area of interest</span>
|
||||
<strong>{selectedMapArea?.name ?? 'No area selected'}</strong>
|
||||
<small>{areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Active layer</span>
|
||||
<strong>{mapLayerLabel}</strong>
|
||||
<small>{mapLayerSourceLabel}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Feature state</span>
|
||||
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'}</strong>
|
||||
<small>{mapLayerProvenance}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>QA/QC evidence</span>
|
||||
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} evidence features` : 'No evidence overlay'}</strong>
|
||||
<small>{qualityEvidenceLoading ? 'Loading persisted evidence' : 'Matches, false positives and false negatives'}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="map-control-surface" aria-label="Map workspace controls">
|
||||
{usesDefaultOsmBasemap ? (
|
||||
<div className="basemap-policy-notice" aria-label="Basemap usage notice">
|
||||
@@ -605,6 +582,53 @@ export function MapWorkspace({
|
||||
<span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="map-frame-surface">
|
||||
<GeoMap
|
||||
data={mapFeatureCollection}
|
||||
areaData={areaFeatureCollection}
|
||||
selectedFeature={selectedFeature}
|
||||
selectionData={mapSelectionResult?.geojson ?? null}
|
||||
qaEvidenceData={qualityEvidenceGeoJson}
|
||||
selectionBbox={mapSelectionBbox}
|
||||
bboxSelectionMode={bboxSelectionMode}
|
||||
visible={mapLayerVisible}
|
||||
opacity={mapLayerOpacity}
|
||||
areaVisible={areaLayerVisible}
|
||||
areaOpacity={areaLayerOpacity}
|
||||
onFeatureSelect={onSelectMapFeature}
|
||||
onMapCoordinateSelect={handleMapCoordinateSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details className="map-layer-details">
|
||||
<summary>
|
||||
<span>Layer details</span>
|
||||
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No active layer'}</strong>
|
||||
</summary>
|
||||
<div className="map-context-summary" aria-label="Map layer status">
|
||||
<div>
|
||||
<span>Area of interest</span>
|
||||
<strong>{selectedMapArea?.name ?? 'No area selected'}</strong>
|
||||
<small>{areaFeatureCollection ? `${areaFeatureCount} AOI features loaded` : 'AOI overlay disabled'}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Active layer</span>
|
||||
<strong>{mapLayerLabel}</strong>
|
||||
<small>{mapLayerSourceLabel}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>Feature state</span>
|
||||
<strong>{mapFeatureCollection ? `${mapFeatureCount} rendered features` : 'No layer rendered'}</strong>
|
||||
<small>{mapLayerProvenance}</small>
|
||||
</div>
|
||||
<div>
|
||||
<span>QA/QC evidence</span>
|
||||
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} evidence features` : 'No evidence overlay'}</strong>
|
||||
<small>{qualityEvidenceLoading ? 'Loading persisted evidence' : 'Matches, false positives and false negatives'}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="layer-provenance-rail" aria-label="Active map layer provenance">
|
||||
<div>
|
||||
<span>Layer source</span>
|
||||
@@ -623,7 +647,7 @@ export function MapWorkspace({
|
||||
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} rendered` : 'off'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? (
|
||||
<div className="qa-evidence-map-status" aria-label="QA/QC evidence map overlay status">
|
||||
@@ -671,24 +695,6 @@ export function MapWorkspace({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="map-frame-surface">
|
||||
<GeoMap
|
||||
data={mapFeatureCollection}
|
||||
areaData={areaFeatureCollection}
|
||||
selectedFeature={selectedFeature}
|
||||
selectionData={mapSelectionResult?.geojson ?? null}
|
||||
qaEvidenceData={qualityEvidenceGeoJson}
|
||||
selectionBbox={mapSelectionBbox}
|
||||
bboxSelectionMode={bboxSelectionMode}
|
||||
visible={mapLayerVisible}
|
||||
opacity={mapLayerOpacity}
|
||||
areaVisible={areaLayerVisible}
|
||||
areaOpacity={areaLayerOpacity}
|
||||
onFeatureSelect={onSelectMapFeature}
|
||||
onMapCoordinateSelect={handleMapCoordinateSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="map-inspection-surface">
|
||||
<div className="gis-test-run-surface" aria-label="Operational GIS test run">
|
||||
<div className="panel-title-row">
|
||||
@@ -863,6 +869,12 @@ export function MapWorkspace({
|
||||
{fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
<details className="map-advanced-tools">
|
||||
<summary>
|
||||
<span>Advanced selection and inspection</span>
|
||||
<strong>BBox, feature extract and raw properties</strong>
|
||||
</summary>
|
||||
<div className="map-advanced-tools-body">
|
||||
<div className="bbox-select-surface" aria-label="Area selection and extract">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
@@ -1208,6 +1220,8 @@ export function MapWorkspace({
|
||||
<p className="muted">Click a visible map feature to inspect its properties.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -49,8 +49,8 @@ export function AreaPanel({
|
||||
<small>{selectedArea?.area_m2 ? `${selectedArea.area_m2.toFixed(2)} m2` : 'Select an AOI with geometry for spatial review.'}</small>
|
||||
</div>
|
||||
|
||||
<div className="data-panel-form-block">
|
||||
<p className="data-section-label">Create spatial scope</p>
|
||||
<details className="data-panel-form-block">
|
||||
<summary>Create spatial scope</summary>
|
||||
<form className="compact-form" onSubmit={onCreateArea}>
|
||||
<label>
|
||||
AOI name
|
||||
@@ -80,7 +80,7 @@ export function AreaPanel({
|
||||
Create area
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{loadingAreas ? <p>Loading areas...</p> : null}
|
||||
{areas.length === 0 ? <p>No areas yet</p> : null}
|
||||
|
||||
@@ -45,8 +45,8 @@ export function ProjectPanel({
|
||||
<small>{selectedProject?.region ?? 'Create or load a project to start the workbench.'}</small>
|
||||
</div>
|
||||
|
||||
<div className="data-panel-form-block">
|
||||
<p className="data-section-label">Create context</p>
|
||||
<details className="data-panel-form-block">
|
||||
<summary>Create project</summary>
|
||||
<form className="compact-form" onSubmit={onCreateProject}>
|
||||
<label>
|
||||
Name
|
||||
@@ -76,7 +76,7 @@ export function ProjectPanel({
|
||||
Create project
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="demo-actions">
|
||||
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
|
||||
|
||||
@@ -116,13 +116,12 @@ export function SegmentationLab({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ai-lab-model-surface" aria-label="Segmentation model capabilities">
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Model registry</h3>
|
||||
<p>Backend-reported segmenter states and limitations.</p>
|
||||
</div>
|
||||
</div>
|
||||
<details className="ai-lab-model-surface" aria-label="Segmentation model capabilities">
|
||||
<summary>
|
||||
<span>Model registry</span>
|
||||
<strong>{segmentationModels.length} models</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-state-stack">
|
||||
{loadingSegmentationModels ? (
|
||||
<div className="result-state result-state-loading">
|
||||
@@ -158,7 +157,8 @@ export function SegmentationLab({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="lab-block">
|
||||
<div className="ai-lab-run-surface" aria-label="Segmentation run controls">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user