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
@@ -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) => (
+56 -42
View File
@@ -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">