Refine premium workbench UX
This commit is contained in:
+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