Refine premium workbench UX
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-14 05:10:36 +02:00
parent a18fba8c7a
commit a2d9cef986
15 changed files with 1802 additions and 106 deletions
+10
View File
@@ -7,6 +7,16 @@
# Changelog # 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) ## 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. - 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
+39
View File
@@ -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. - 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. - 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. - 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.
+8
View File
@@ -402,6 +402,14 @@ This file now starts with the current implementation status. Older preparation/b
# Current Workbench UI # 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] Replace the one-page workflow panel stack with a task-based workbench shell.
- [x] Add persistent project/AOI/dataset/layer context. - [x] Add persistent project/AOI/dataset/layer context.
- [x] Move selected dataset details into a persistent inspector. - [x] Move selected dataset details into a persistent inspector.
+8 -7
View File
@@ -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. 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. 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. - 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. - 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. - 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 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 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. - 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-topbar`: active project, AOI, dataset and layer context.
- `workbench-sidebar`: primary navigation between workflow workspaces. - `workbench-sidebar`: primary navigation between workflow workspaces.
- `workbench-main`: one active workflow at a time. - `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. - Existing API calls, hooks, MapLibre rendering and QA/AI/export flows are unchanged.
- Stable navigation test anchors use `data-testid="workspace-nav-{workspace}"`. - 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. - 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. - 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. - 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.
+48 -12
View File
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import './styles/app.css' import './styles/app.css'
import './styles/premium.css'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { DatasetPanel } from './components/datasets/DatasetPanel' import { DatasetPanel } from './components/datasets/DatasetPanel'
import { DetectionLab } from './components/detection/DetectionLab' 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' }, { 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 { function App(): JSX.Element {
const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview') const [activeWorkspace, setActiveWorkspace] = useState<WorkspaceKey>('overview')
const [inspectorOpen, setInspectorOpen] = useState(false)
useEffect(() => { useEffect(() => {
window.scrollTo({ top: 0, left: 0 }) window.scrollTo({ top: 0, left: 0 })
setInspectorOpen(false)
}, [activeWorkspace]) }, [activeWorkspace])
const { const {
@@ -643,8 +652,11 @@ function App(): JSX.Element {
</a> </a>
<header className="workbench-topbar"> <header className="workbench-topbar">
<div className="brand-block"> <div className="brand-block">
<p className="eyebrow">GeoAI Workbench · Mol / Kempen</p> <span className="brand-mark" aria-hidden="true">GI</span>
<h1>GeoIntel Kempen</h1> <div>
<h1>GeoIntel</h1>
<p>GeoAI Workbench · Mol / Kempen</p>
</div>
</div> </div>
<div className="context-bar" aria-label="Active workbench context"> <div className="context-bar" aria-label="Active workbench context">
<div> <div>
@@ -671,7 +683,15 @@ function App(): JSX.Element {
<div className="workbench-layout"> <div className="workbench-layout">
<aside className="workbench-sidebar" aria-label="Workbench navigation"> <aside className="workbench-sidebar" aria-label="Workbench navigation">
<nav aria-label="Primary workspaces"> <nav aria-label="Primary workspaces">
{workspaceNavItems.map((item) => ( {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 <button
key={item.key} key={item.key}
type="button" type="button"
@@ -684,17 +704,33 @@ function App(): JSX.Element {
<span>{item.label}</span> <span>{item.label}</span>
<small>{item.description}</small> <small>{item.description}</small>
</button> </button>
)
})}
</div>
))} ))}
</nav> </nav>
</aside> </aside>
<main className="workbench-main" id="workspace-main" tabIndex={-1}> <main className="workbench-main" id="workspace-main" tabIndex={-1}>
<div className="workspace-heading"> <div className="workspace-heading">
<div> <div className="workspace-heading-copy">
<p className="eyebrow">Workspace</p> <p className="eyebrow">{selectedProject?.region ?? 'Mol, Kempen'}</p>
<h2>{activeWorkspaceItem.label}</h2> <h2>{activeWorkspaceItem.label}</h2>
</div> </div>
<div className="workspace-heading-actions">
<p>{activeWorkspaceItem.description}</p> <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>
<div className="workspace-command-bar" aria-label="Workspace shortcuts"> <div className="workspace-command-bar" aria-label="Workspace shortcuts">
<div className="workspace-nav-cluster"> <div className="workspace-nav-cluster">
@@ -763,12 +799,8 @@ function App(): JSX.Element {
</section> </section>
<section className="overview-actions"> <section className="overview-actions">
<div className="overview-action-copy"> <div className="overview-action-copy">
<p className="eyebrow">Recommended flow</p> <p className="eyebrow">Quick access</p>
<h2>Build the project, load data, review on map, validate, export.</h2> <h2>Continue working</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>
</div> </div>
<div className="quick-action-grid overview-quick-actions" aria-label="Recommended next workbench actions"> <div className="quick-action-grid overview-quick-actions" aria-label="Recommended next workbench actions">
<button <button
@@ -845,6 +877,7 @@ function App(): JSX.Element {
onDatasetFormChange={setDatasetForm} onDatasetFormChange={setDatasetForm}
onUploadDataset={uploadDataset} onUploadDataset={uploadDataset}
onLoadDatasetDetails={loadDatasetDetails} onLoadDatasetDetails={loadDatasetDetails}
onOpenInspector={() => setInspectorOpen(true)}
onRefreshMetadata={refreshMetadata} onRefreshMetadata={refreshMetadata}
onOpenDatasetInMap={openDatasetInMap} onOpenDatasetInMap={openDatasetInMap}
onOpenDatasetExport={openDatasetExport} onOpenDatasetExport={openDatasetExport}
@@ -1080,7 +1113,8 @@ function App(): JSX.Element {
) : null} ) : null}
</main> </main>
<aside className="workbench-inspector" aria-label="Current selection inspector"> {inspectorOpen ? (
<aside className="workbench-inspector" id="workbench-inspector" aria-label="Current selection inspector">
<WorkbenchInspector <WorkbenchInspector
selectedProject={selectedProject} selectedProject={selectedProject}
selectedArea={selectedArea} selectedArea={selectedArea}
@@ -1101,6 +1135,7 @@ function App(): JSX.Element {
onOpenQualityWorkspace={() => setActiveWorkspace('analysis')} onOpenQualityWorkspace={() => setActiveWorkspace('analysis')}
onOpenExportsWorkspace={() => setActiveWorkspace('exports')} onOpenExportsWorkspace={() => setActiveWorkspace('exports')}
onOpenAiWorkspace={() => setActiveWorkspace('ai')} onOpenAiWorkspace={() => setActiveWorkspace('ai')}
onClose={() => setInspectorOpen(false)}
datasetDetailProps={{ datasetDetailProps={{
areas, areas,
availableVectorTargets, availableVectorTargets,
@@ -1162,6 +1197,7 @@ function App(): JSX.Element {
}} }}
/> />
</aside> </aside>
) : null}
</div> </div>
</div> </div>
) )
@@ -105,16 +105,24 @@ export function WorkbenchStatusStrip({
state: exports.length > 0 ? 'ready' : 'waiting', state: exports.length > 0 ? 'ready' : 'waiting',
}, },
] ]
const readyCount = items.filter((item) => item.state === 'ready').length
return ( return (
<section className="workbench-status-strip" aria-label="Workbench readiness"> <section className="workbench-status-strip" aria-label="Workbench readiness">
<div className="status-strip-header"> <div className="status-strip-header">
<div> <div>
<p className="eyebrow">V1 readiness</p> <p className="eyebrow">V1 readiness</p>
<h2>Workbench status</h2> <h2>
</div> <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> <p className="status-next-action">{nextAction(items)}</p>
</div> </div>
<div className="status-readiness" aria-label={`${readyCount} of ${items.length} workbench stages ready`}>
<strong>{readyCount}/{items.length}</strong>
<span>stages ready</span>
</div>
</div>
<div className="status-strip-grid"> <div className="status-strip-grid">
{items.map((item) => ( {items.map((item) => (
<div className={`status-tile status-tile-${item.state}`} key={item.key}> <div className={`status-tile status-tile-${item.state}`} key={item.key}>
@@ -23,6 +23,7 @@ interface DatasetPanelProps {
onDatasetFormChange: Dispatch<SetStateAction<DatasetFormState>> onDatasetFormChange: Dispatch<SetStateAction<DatasetFormState>>
onUploadDataset: (event: FormEvent) => void onUploadDataset: (event: FormEvent) => void
onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void onLoadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => void
onOpenInspector: () => void
onRefreshMetadata: (datasetId: string) => void onRefreshMetadata: (datasetId: string) => void
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
onOpenDatasetExport: (dataset: DatasetCreateResponse) => void onOpenDatasetExport: (dataset: DatasetCreateResponse) => void
@@ -100,6 +101,7 @@ export function DatasetPanel({
onDatasetFormChange, onDatasetFormChange,
onUploadDataset, onUploadDataset,
onLoadDatasetDetails, onLoadDatasetDetails,
onOpenInspector,
onRefreshMetadata, onRefreshMetadata,
onOpenDatasetInMap, onOpenDatasetInMap,
onOpenDatasetExport, onOpenDatasetExport,
@@ -153,8 +155,8 @@ export function DatasetPanel({
<small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}</small> <small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}</small>
</div> </div>
<div className="dataset-upload-block data-panel-form-block"> <details className="dataset-upload-block data-panel-form-block">
<p className="data-section-label">Upload source data</p> <summary>Upload source data</summary>
<form className="dataset-upload-form" onSubmit={onUploadDataset}> <form className="dataset-upload-form" onSubmit={onUploadDataset}>
<label> <label>
Type Type
@@ -201,7 +203,7 @@ export function DatasetPanel({
Upload dataset Upload dataset
</button> </button>
</form> </form>
</div> </details>
<div className="dataset-role-summary-grid" aria-label="Dataset role summary"> <div className="dataset-role-summary-grid" aria-label="Dataset role summary">
{roleSummaries.map((summary) => ( {roleSummaries.map((summary) => (
@@ -255,6 +257,7 @@ export function DatasetPanel({
<button <button
className="primary-action dataset-action-button" className="primary-action dataset-action-button"
type="button" type="button"
onClickCapture={onOpenInspector}
onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)} onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}
data-testid={`dataset-select-${dataset.id}`} data-testid={`dataset-select-${dataset.id}`}
> >
@@ -206,13 +206,12 @@ export function DetectionLab({
</button> </button>
</div> </div>
<div className="ai-lab-model-surface" aria-label="Detection model capabilities"> <details className="ai-lab-model-surface" aria-label="Detection model capabilities">
<div className="ai-lab-section-header"> <summary>
<div> <span>Model registry</span>
<h3>Model registry</h3> <strong>{detectionModels.length} models</strong>
<p>Backend-reported detector states and limitations.</p> </summary>
</div> <div className="ai-lab-disclosure-body">
</div>
<div className="ai-lab-state-stack"> <div className="ai-lab-state-stack">
{loadingDetectionModels ? ( {loadingDetectionModels ? (
<div className="result-state result-state-loading"> <div className="result-state result-state-loading">
@@ -255,6 +254,7 @@ export function DetectionLab({
))} ))}
</ul> </ul>
</div> </div>
</details>
{selectedDetectionModelId === 'yolo-configured' ? ( {selectedDetectionModelId === 'yolo-configured' ? (
<div className="ai-lab-model-surface" aria-label="Local model asset selection"> <div className="ai-lab-model-surface" aria-label="Local model asset selection">
@@ -359,7 +359,12 @@ export function DetectionLab({
</div> </div>
) : null} ) : 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 className="ai-lab-section-header">
<div> <div>
<h3>YOLO runtime preflight</h3> <h3>YOLO runtime preflight</h3>
@@ -438,6 +443,7 @@ export function DetectionLab({
</div> </div>
) : null} ) : null}
</div> </div>
</details>
<div className="lab-block"> <div className="lab-block">
<div className="ai-lab-run-surface" aria-label="Detection run controls"> <div className="ai-lab-run-surface" aria-label="Detection run controls">
@@ -35,6 +35,7 @@ interface WorkbenchInspectorProps {
onOpenQualityWorkspace: () => void onOpenQualityWorkspace: () => void
onOpenExportsWorkspace: () => void onOpenExportsWorkspace: () => void
onOpenAiWorkspace: () => void onOpenAiWorkspace: () => void
onClose: () => void
} }
function formatValue(value: string | number | null | undefined): string { function formatValue(value: string | number | null | undefined): string {
@@ -74,6 +75,7 @@ export function WorkbenchInspector({
onOpenQualityWorkspace, onOpenQualityWorkspace,
onOpenExportsWorkspace, onOpenExportsWorkspace,
onOpenAiWorkspace, onOpenAiWorkspace,
onClose,
}: WorkbenchInspectorProps): JSX.Element { }: WorkbenchInspectorProps): JSX.Element {
const [activeTab, setActiveTab] = useState<InspectorTab>('context') const [activeTab, setActiveTab] = useState<InspectorTab>('context')
const selectedDetectionRun = useMemo( const selectedDetectionRun = useMemo(
@@ -103,6 +105,9 @@ export function WorkbenchInspector({
<p className="eyebrow">Inspector</p> <p className="eyebrow">Inspector</p>
<h2>Selection details</h2> <h2>Selection details</h2>
</div> </div>
<button type="button" className="inspector-close" onClick={onClose} aria-label="Close selection details">
Close
</button>
</div> </div>
<div className="inspector-tabs" role="tablist" aria-label="Inspector detail tabs"> <div className="inspector-tabs" role="tablist" aria-label="Inspector detail tabs">
{tabs.map((tab) => ( {tabs.map((tab) => (
+56 -42
View File
@@ -489,29 +489,6 @@ export function MapWorkspace({
<span className="count-pill">{mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}</span> <span className="count-pill">{mapFeatureCollection ? `${mapFeatureCount} features` : 'no layer'}</span>
</div> </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"> <div className="map-control-surface" aria-label="Map workspace controls">
{usesDefaultOsmBasemap ? ( {usesDefaultOsmBasemap ? (
<div className="basemap-policy-notice" aria-label="Basemap usage notice"> <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> <span>{mapFeatureCollection ? `${mapFeatureCount} features loaded` : 'No vector/result layer loaded'}</span>
</div> </div>
</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 className="layer-provenance-rail" aria-label="Active map layer provenance">
<div> <div>
<span>Layer source</span> <span>Layer source</span>
@@ -623,7 +647,7 @@ export function MapWorkspace({
<strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} rendered` : 'off'}</strong> <strong>{qualityEvidenceGeoJson ? `${qualityEvidenceFeatureCount} rendered` : 'off'}</strong>
</div> </div>
</div> </div>
</div> </details>
{qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? ( {qualityEvidenceGeoJson || qualityEvidenceError || qualityEvidenceWarnings.length > 0 ? (
<div className="qa-evidence-map-status" aria-label="QA/QC evidence map overlay status"> <div className="qa-evidence-map-status" aria-label="QA/QC evidence map overlay status">
@@ -671,24 +695,6 @@ export function MapWorkspace({
</div> </div>
) : null} ) : 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="map-inspection-surface">
<div className="gis-test-run-surface" aria-label="Operational GIS test run"> <div className="gis-test-run-surface" aria-label="Operational GIS test run">
<div className="panel-title-row"> <div className="panel-title-row">
@@ -863,6 +869,12 @@ export function MapWorkspace({
{fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null} {fullWorkflowError ? <p className="error">{fullWorkflowError}</p> : null}
</div> </div>
</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="bbox-select-surface" aria-label="Area selection and extract">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
@@ -1209,6 +1221,8 @@ export function MapWorkspace({
)} )}
</div> </div>
</div> </div>
</details>
</div>
</section> </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> <small>{selectedArea?.area_m2 ? `${selectedArea.area_m2.toFixed(2)} m2` : 'Select an AOI with geometry for spatial review.'}</small>
</div> </div>
<div className="data-panel-form-block"> <details className="data-panel-form-block">
<p className="data-section-label">Create spatial scope</p> <summary>Create spatial scope</summary>
<form className="compact-form" onSubmit={onCreateArea}> <form className="compact-form" onSubmit={onCreateArea}>
<label> <label>
AOI name AOI name
@@ -80,7 +80,7 @@ export function AreaPanel({
Create area Create area
</button> </button>
</form> </form>
</div> </details>
{loadingAreas ? <p>Loading areas...</p> : null} {loadingAreas ? <p>Loading areas...</p> : null}
{areas.length === 0 ? <p>No areas yet</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> <small>{selectedProject?.region ?? 'Create or load a project to start the workbench.'}</small>
</div> </div>
<div className="data-panel-form-block"> <details className="data-panel-form-block">
<p className="data-section-label">Create context</p> <summary>Create project</summary>
<form className="compact-form" onSubmit={onCreateProject}> <form className="compact-form" onSubmit={onCreateProject}>
<label> <label>
Name Name
@@ -76,7 +76,7 @@ export function ProjectPanel({
Create project Create project
</button> </button>
</form> </form>
</div> </details>
<div className="demo-actions"> <div className="demo-actions">
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow"> <button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
@@ -116,13 +116,12 @@ export function SegmentationLab({
</button> </button>
</div> </div>
<div className="ai-lab-model-surface" aria-label="Segmentation model capabilities"> <details className="ai-lab-model-surface" aria-label="Segmentation model capabilities">
<div className="ai-lab-section-header"> <summary>
<div> <span>Model registry</span>
<h3>Model registry</h3> <strong>{segmentationModels.length} models</strong>
<p>Backend-reported segmenter states and limitations.</p> </summary>
</div> <div className="ai-lab-disclosure-body">
</div>
<div className="ai-lab-state-stack"> <div className="ai-lab-state-stack">
{loadingSegmentationModels ? ( {loadingSegmentationModels ? (
<div className="result-state result-state-loading"> <div className="result-state result-state-loading">
@@ -159,6 +158,7 @@ export function SegmentationLab({
))} ))}
</ul> </ul>
</div> </div>
</details>
<div className="lab-block"> <div className="lab-block">
<div className="ai-lab-run-surface" aria-label="Segmentation run controls"> <div className="ai-lab-run-surface" aria-label="Segmentation run controls">
File diff suppressed because it is too large Load Diff