feat: simplify regional end user workbench
This commit is contained in:
+100
-79
@@ -30,6 +30,8 @@ import { useQualityWorkflow } from './hooks/useQualityWorkflow'
|
||||
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
|
||||
import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap'
|
||||
import { useViewportVectorLayer } from './hooks/useViewportVectorLayer'
|
||||
import { getDatasetDisplayName } from './lib/datasetDisplay'
|
||||
import { REGIONAL_WORKSPACE_LABEL, REGIONAL_WORKSPACE_PROJECT_NAME } from './config/primaryFocus'
|
||||
|
||||
function isVectorDatasetType(datasetType: string): boolean {
|
||||
return datasetType === 'vector' || datasetType === 'geojson'
|
||||
@@ -38,19 +40,20 @@ function isVectorDatasetType(datasetType: string): boolean {
|
||||
type WorkspaceKey = 'overview' | 'data' | 'map' | 'analysis' | 'ai' | 'exports' | 'system'
|
||||
|
||||
const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [
|
||||
{ key: 'overview', label: 'Overview', description: 'V1 readiness and next action' },
|
||||
{ key: 'data', label: 'Data', description: 'Projects, AOI and datasets' },
|
||||
{ key: 'map', label: 'Map', description: 'Layer review and inspection' },
|
||||
{ key: 'analysis', label: 'QA/QC', description: 'Quality checks and change detection' },
|
||||
{ key: 'ai', label: 'AI Labs', description: 'Detection and segmentation' },
|
||||
{ key: 'exports', label: 'Exports', description: 'Artifacts and downloads' },
|
||||
{ key: 'system', label: 'System', description: 'Provider capabilities' },
|
||||
{ key: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' },
|
||||
{ key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' },
|
||||
{ key: 'map', label: 'Kaart', description: 'Selecteren, uitlezen en vergelijken' },
|
||||
{ key: 'analysis', label: 'Kwaliteit', description: 'Resultaten controleren' },
|
||||
{ key: 'ai', label: 'Beeldanalyse', description: 'Gebouwen herkennen op luchtbeelden' },
|
||||
{ key: 'exports', label: 'Downloads', description: 'Resultaten bewaren en delen' },
|
||||
{ key: 'system', label: 'Beheer', description: 'Databronnen en systeemstatus' },
|
||||
]
|
||||
|
||||
const workspaceNavGroups: Array<{ label: string; keys: WorkspaceKey[] }> = [
|
||||
{ label: 'Workspace', keys: ['overview', 'data', 'map'] },
|
||||
{ label: 'Analyze', keys: ['analysis', 'ai'] },
|
||||
{ label: 'Deliver', keys: ['exports', 'system'] },
|
||||
{ label: 'Verkennen', keys: ['map', 'data'] },
|
||||
{ label: 'Analyseren', keys: ['analysis', 'ai'] },
|
||||
{ label: 'Afronden', keys: ['exports'] },
|
||||
{ label: 'Beheer', keys: ['overview', 'system'] },
|
||||
]
|
||||
|
||||
function App(): JSX.Element {
|
||||
@@ -659,43 +662,45 @@ function App(): JSX.Element {
|
||||
target: 'exports',
|
||||
},
|
||||
]
|
||||
const projectContextLabel = selectedProject?.name ?? 'No project'
|
||||
const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Select area' : 'No AOI')
|
||||
const datasetContextLabel = selectedDataset?.name ?? (datasets.length > 0 ? 'Select dataset' : 'No dataset')
|
||||
const projectContextLabel = selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME
|
||||
? REGIONAL_WORKSPACE_LABEL
|
||||
: selectedProject?.name ?? 'Geen werkruimte'
|
||||
const areaContextLabel = selectedArea?.name ?? (areas.length > 0 ? 'Kies een gebied' : 'Geen gebied')
|
||||
const datasetContextLabel = selectedDataset ? getDatasetDisplayName(selectedDataset) : datasets.length > 0 ? 'Kies een bron' : 'Geen bron'
|
||||
const layerContextLabel = mapFeatureCollection
|
||||
? `${mapFeatureCount} features`
|
||||
: viewportVectorLayerActive
|
||||
? 'Viewport layer selected'
|
||||
: 'No active layer'
|
||||
? 'Kaartlaag gekozen'
|
||||
: 'Geen actieve laag'
|
||||
|
||||
return (
|
||||
<div className="app-shell workbench-shell">
|
||||
<a className="skip-link" href="#workspace-main">
|
||||
Skip to workspace
|
||||
Ga naar de werkruimte
|
||||
</a>
|
||||
<header className="workbench-topbar">
|
||||
<div className="brand-block">
|
||||
<span className="brand-mark" aria-hidden="true">GI</span>
|
||||
<div>
|
||||
<h1>GeoIntel</h1>
|
||||
<p>GeoAI Workbench · Mol / Kempen</p>
|
||||
<p>GeoAI-werkruimte · Mol en de Kempen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="context-bar" aria-label="Active workbench context">
|
||||
<div>
|
||||
<span>Project</span>
|
||||
<span>Werkruimte</span>
|
||||
<strong title={projectContextLabel}>{projectContextLabel}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>AOI</span>
|
||||
<span>Gebied</span>
|
||||
<strong title={areaContextLabel}>{areaContextLabel}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Dataset</span>
|
||||
<span>Bron</span>
|
||||
<strong title={datasetContextLabel}>{datasetContextLabel}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Layer</span>
|
||||
<span>Kaartlaag</span>
|
||||
<strong title={layerContextLabel}>{layerContextLabel}</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -770,7 +775,7 @@ function App(): JSX.Element {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span>{selectedProject ? `Active project: ${selectedProject.name}` : 'Start with a project or load the demo workflow.'}</span>
|
||||
<span>{selectedProject ? `Actief bereik: ${projectContextLabel}` : 'De regionale gegevens worden ingeladen.'}</span>
|
||||
</div>
|
||||
|
||||
{activeWorkspace === 'overview' ? (
|
||||
@@ -784,6 +789,11 @@ function App(): JSX.Element {
|
||||
activeLayerFeatureCount={mapFeatureCount}
|
||||
selectedAreaHasGeometry={Boolean(areaFeatureCollection)}
|
||||
/>
|
||||
<details className="status-details-disclosure">
|
||||
<summary>
|
||||
<span>Volledige workflowstatus</span>
|
||||
<strong>{workflowGuidanceComplete ? 'voltooid' : 'optionele stappen open'}</strong>
|
||||
</summary>
|
||||
<section className="workflow-guidance-panel" aria-label="V1 workflow guidance">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
@@ -860,6 +870,7 @@ function App(): JSX.Element {
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</details>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -958,7 +969,6 @@ function App(): JSX.Element {
|
||||
selectedMapDatasetId={selectedDataset && isVectorDatasetType(selectedDataset.dataset_type) ? selectedDataset.id : ''}
|
||||
selectedFeature={selectedMapFeature}
|
||||
onSelectMapArea={setSelectedMapAreaId}
|
||||
onSelectProject={selectProject}
|
||||
onOpenDatasetInMap={openDatasetInMap}
|
||||
onSetAreaLayerVisible={setAreaLayerVisible}
|
||||
onSetAreaLayerOpacity={setAreaLayerOpacity}
|
||||
@@ -981,22 +991,6 @@ function App(): JSX.Element {
|
||||
|
||||
{activeWorkspace === 'analysis' ? (
|
||||
<div className="workspace-grid workspace-grid-analysis">
|
||||
<ChangeDetectionPanel
|
||||
vectorDatasets={availableVectorDatasets}
|
||||
sourceDatasetId={changeSourceDatasetId}
|
||||
targetDatasetId={changeTargetDatasetId}
|
||||
iouThreshold={changeIouThreshold}
|
||||
includeUnchanged={changeIncludeUnchanged}
|
||||
running={runningChangeDetection}
|
||||
result={changeDetectionResult}
|
||||
error={changeDetectionError}
|
||||
onSourceDatasetChange={setChangeSourceDatasetId}
|
||||
onTargetDatasetChange={setChangeTargetDatasetId}
|
||||
onIouThresholdChange={setChangeIouThreshold}
|
||||
onIncludeUnchangedChange={setChangeIncludeUnchanged}
|
||||
onRun={runChangeDetection}
|
||||
/>
|
||||
|
||||
<QualityResultsPanel
|
||||
selectedProjectId={selectedProjectId}
|
||||
qualityChecks={qualityChecks}
|
||||
@@ -1008,6 +1002,27 @@ function App(): JSX.Element {
|
||||
evidenceLoading={qualityEvidenceLoading}
|
||||
evidenceError={qualityEvidenceError}
|
||||
/>
|
||||
<details className="secondary-analysis-disclosure">
|
||||
<summary>
|
||||
<span>Historische vectorlagen vergelijken</span>
|
||||
<strong>Geavanceerd</strong>
|
||||
</summary>
|
||||
<ChangeDetectionPanel
|
||||
vectorDatasets={availableVectorDatasets}
|
||||
sourceDatasetId={changeSourceDatasetId}
|
||||
targetDatasetId={changeTargetDatasetId}
|
||||
iouThreshold={changeIouThreshold}
|
||||
includeUnchanged={changeIncludeUnchanged}
|
||||
running={runningChangeDetection}
|
||||
result={changeDetectionResult}
|
||||
error={changeDetectionError}
|
||||
onSourceDatasetChange={setChangeSourceDatasetId}
|
||||
onTargetDatasetChange={setChangeTargetDatasetId}
|
||||
onIouThresholdChange={setChangeIouThreshold}
|
||||
onIncludeUnchangedChange={setChangeIncludeUnchanged}
|
||||
onRun={runChangeDetection}
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -1069,46 +1084,52 @@ function App(): JSX.Element {
|
||||
onApplyOperatorProfile={applyDetectionOperatorProfile}
|
||||
/>
|
||||
|
||||
<SegmentationLab
|
||||
segmentationModels={segmentationModels}
|
||||
loadingSegmentationModels={loadingSegmentationModels}
|
||||
segmentationModelError={segmentationModelError}
|
||||
selectedSegmentationDatasetId={selectedSegmentationDatasetId}
|
||||
selectedSegmentationModelId={selectedSegmentationModelId}
|
||||
segmentationTileManifestPath={segmentationTileManifestPath}
|
||||
segmentationConfidenceThreshold={segmentationConfidenceThreshold}
|
||||
runningSegmentation={runningSegmentation}
|
||||
segmentationRunResult={segmentationRunResult}
|
||||
segmentationRunError={segmentationRunError}
|
||||
segmentationRuns={segmentationRuns}
|
||||
selectedSegmentationRunId={selectedSegmentationRunId}
|
||||
segmentationItems={segmentationItems}
|
||||
segmentationClassFilter={segmentationClassFilter}
|
||||
segmentationMinConfidenceFilter={segmentationMinConfidenceFilter}
|
||||
loadingSegmentationResults={loadingSegmentationResults}
|
||||
segmentationReferenceDatasetId={segmentationReferenceDatasetId}
|
||||
segmentationQaResult={segmentationQaResult}
|
||||
segmentationQaError={segmentationQaError}
|
||||
runningSegmentationQa={runningSegmentationQa}
|
||||
selectedProjectId={selectedProjectId}
|
||||
rasterDatasets={rasterDatasets}
|
||||
referenceDatasets={referenceDatasets}
|
||||
selectedSegmentationModelConfigured={Boolean(selectedSegmentationModel?.configured)}
|
||||
selectedSegmentationModelLimitation={selectedSegmentationModel?.limitation_message ?? null}
|
||||
onLoadModels={loadSegmentationModels}
|
||||
onSelectDataset={setSelectedSegmentationDatasetId}
|
||||
onSelectModel={setSelectedSegmentationModelId}
|
||||
onSetTileManifestPath={setSegmentationTileManifestPath}
|
||||
onSetConfidenceThreshold={setSegmentationConfidenceThreshold}
|
||||
onRunSegmentation={runSegmentation}
|
||||
onLoadRuns={() => loadSegmentationRuns()}
|
||||
onSelectRun={setSelectedSegmentationRunId}
|
||||
onSetClassFilter={setSegmentationClassFilter}
|
||||
onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter}
|
||||
onLoadResults={() => loadSegmentationResults()}
|
||||
onSelectReferenceDataset={setSegmentationReferenceDatasetId}
|
||||
onRunQa={runSegmentationQa}
|
||||
/>
|
||||
<details className="secondary-analysis-disclosure segmentation-disclosure">
|
||||
<summary>
|
||||
<span>Segmentatie van beeldvlakken</span>
|
||||
<strong>Nog niet geconfigureerd</strong>
|
||||
</summary>
|
||||
<SegmentationLab
|
||||
segmentationModels={segmentationModels}
|
||||
loadingSegmentationModels={loadingSegmentationModels}
|
||||
segmentationModelError={segmentationModelError}
|
||||
selectedSegmentationDatasetId={selectedSegmentationDatasetId}
|
||||
selectedSegmentationModelId={selectedSegmentationModelId}
|
||||
segmentationTileManifestPath={segmentationTileManifestPath}
|
||||
segmentationConfidenceThreshold={segmentationConfidenceThreshold}
|
||||
runningSegmentation={runningSegmentation}
|
||||
segmentationRunResult={segmentationRunResult}
|
||||
segmentationRunError={segmentationRunError}
|
||||
segmentationRuns={segmentationRuns}
|
||||
selectedSegmentationRunId={selectedSegmentationRunId}
|
||||
segmentationItems={segmentationItems}
|
||||
segmentationClassFilter={segmentationClassFilter}
|
||||
segmentationMinConfidenceFilter={segmentationMinConfidenceFilter}
|
||||
loadingSegmentationResults={loadingSegmentationResults}
|
||||
segmentationReferenceDatasetId={segmentationReferenceDatasetId}
|
||||
segmentationQaResult={segmentationQaResult}
|
||||
segmentationQaError={segmentationQaError}
|
||||
runningSegmentationQa={runningSegmentationQa}
|
||||
selectedProjectId={selectedProjectId}
|
||||
rasterDatasets={rasterDatasets}
|
||||
referenceDatasets={referenceDatasets}
|
||||
selectedSegmentationModelConfigured={Boolean(selectedSegmentationModel?.configured)}
|
||||
selectedSegmentationModelLimitation={selectedSegmentationModel?.limitation_message ?? null}
|
||||
onLoadModels={loadSegmentationModels}
|
||||
onSelectDataset={setSelectedSegmentationDatasetId}
|
||||
onSelectModel={setSelectedSegmentationModelId}
|
||||
onSetTileManifestPath={setSegmentationTileManifestPath}
|
||||
onSetConfidenceThreshold={setSegmentationConfidenceThreshold}
|
||||
onRunSegmentation={runSegmentation}
|
||||
onLoadRuns={() => loadSegmentationRuns()}
|
||||
onSelectRun={setSelectedSegmentationRunId}
|
||||
onSetClassFilter={setSegmentationClassFilter}
|
||||
onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter}
|
||||
onLoadResults={() => loadSegmentationResults()}
|
||||
onSelectReferenceDataset={setSegmentationReferenceDatasetId}
|
||||
onRunQa={runSegmentationQa}
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -28,21 +28,21 @@ function countReferenceDatasets(datasets: DatasetCreateResponse[]): number {
|
||||
|
||||
function nextAction(items: StatusItem[]): string {
|
||||
if (!items.some((item) => item.key === 'project' && item.state === 'ready')) {
|
||||
return 'Create or open a project.'
|
||||
return 'De regionale werkruimte kon niet worden geopend.'
|
||||
}
|
||||
if (!items.some((item) => item.key === 'area' && item.state === 'ready')) {
|
||||
return 'Create an AOI or select an area with geometry.'
|
||||
return 'Kies een gemeente of de volledige regio.'
|
||||
}
|
||||
if (!items.some((item) => item.key === 'datasets' && item.state === 'ready')) {
|
||||
return 'Upload a vector or raster dataset, or load the offline demo workflow.'
|
||||
return 'Laad een kaartlaag of luchtbeeld in.'
|
||||
}
|
||||
if (!items.some((item) => item.key === 'qa' && item.state === 'ready')) {
|
||||
return 'Run QA/QC against a reference dataset when candidate and reference data are available.'
|
||||
return 'Kaart en bronnen zijn klaar. Kwaliteitscontrole is beschikbaar zodra er een analyseresultaat is.'
|
||||
}
|
||||
if (!items.some((item) => item.key === 'exports' && item.state === 'ready')) {
|
||||
return 'Create a metadata, report or GeoJSON export for handoff.'
|
||||
return 'De analyse is klaar; maak eventueel een rapport of download.'
|
||||
}
|
||||
return 'Workbench has the core V1 loop populated.'
|
||||
return 'Alle onderdelen van de werkruimte bevatten bewaarde resultaten.'
|
||||
}
|
||||
|
||||
export function WorkbenchStatusStrip({
|
||||
@@ -64,63 +64,64 @@ export function WorkbenchStatusStrip({
|
||||
const items: StatusItem[] = [
|
||||
{
|
||||
key: 'project',
|
||||
label: 'Project',
|
||||
value: selectedProject ? selectedProject.name : 'No project',
|
||||
detail: selectedProject ? selectedProject.region : 'Open or create a project',
|
||||
label: 'Werkruimte',
|
||||
value: selectedProject ? selectedProject.name : 'Geen werkruimte',
|
||||
detail: selectedProject ? selectedProject.region : 'Niet geladen',
|
||||
state: selectedProject ? 'ready' : 'waiting',
|
||||
},
|
||||
{
|
||||
key: 'area',
|
||||
label: 'AOI',
|
||||
value: `${areas.length} area${areas.length === 1 ? '' : 's'}`,
|
||||
detail: selectedAreaHasGeometry ? 'selected area has map geometry' : 'no selected map geometry',
|
||||
label: 'Gebieden',
|
||||
value: `${areas.length} beschikbaar`,
|
||||
detail: selectedAreaHasGeometry ? 'Kaartgrens beschikbaar' : 'Geen kaartgrens geselecteerd',
|
||||
state: selectedAreaHasGeometry ? 'ready' : areas.length > 0 ? 'warning' : 'waiting',
|
||||
},
|
||||
{
|
||||
key: 'datasets',
|
||||
label: 'Datasets',
|
||||
value: `${readyDatasets}/${datasets.length} ready`,
|
||||
detail: `${vectorDatasets} vector, ${rasterDatasets} raster, ${referenceDatasets} reference`,
|
||||
label: 'Bronnen',
|
||||
value: `${readyDatasets}/${datasets.length} beschikbaar`,
|
||||
detail: `${vectorDatasets} kaartlagen, ${rasterDatasets} luchtbeelden, ${referenceDatasets} officiële referenties`,
|
||||
state: datasets.length === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning',
|
||||
},
|
||||
{
|
||||
key: 'map',
|
||||
label: 'Map',
|
||||
value: `${activeLayerFeatureCount} feature${activeLayerFeatureCount === 1 ? '' : 's'}`,
|
||||
detail: selectedAreaHasGeometry ? 'AOI overlay available' : 'AOI overlay missing',
|
||||
label: 'Kaart',
|
||||
value: `${activeLayerFeatureCount} zichtbare objecten`,
|
||||
detail: selectedAreaHasGeometry ? 'Gebiedsgrens beschikbaar' : 'Gebiedsgrens ontbreekt',
|
||||
state: selectedAreaHasGeometry || activeLayerFeatureCount > 0 ? 'ready' : 'waiting',
|
||||
},
|
||||
{
|
||||
key: 'qa',
|
||||
label: 'QA/QC',
|
||||
value: `${qualityChecks.length} check${qualityChecks.length === 1 ? '' : 's'}`,
|
||||
detail: latestQualityCheck ? `${latestQualityCheck.check_type}: ${latestQualityCheck.status}` : 'no persisted QA/QC result',
|
||||
label: 'Kwaliteit',
|
||||
value: `${qualityChecks.length} controles`,
|
||||
detail: latestQualityCheck ? `${latestQualityCheck.check_type}: ${latestQualityCheck.status}` : 'Nog geen controle uitgevoerd',
|
||||
state: qualityChecks.length > 0 ? 'ready' : 'waiting',
|
||||
},
|
||||
{
|
||||
key: 'exports',
|
||||
label: 'Exports',
|
||||
value: `${exports.length} export${exports.length === 1 ? '' : 's'}`,
|
||||
detail: latestExport ? `${latestExport.export_type}: ${latestExport.status}` : 'no handoff artifact yet',
|
||||
label: 'Downloads',
|
||||
value: `${exports.length} bestanden`,
|
||||
detail: latestExport ? `${latestExport.export_type}: ${latestExport.status}` : 'Nog geen bestand gemaakt',
|
||||
state: exports.length > 0 ? 'ready' : 'waiting',
|
||||
},
|
||||
]
|
||||
const readyCount = items.filter((item) => item.state === 'ready').length
|
||||
const coreReady = items.slice(0, 4).every((item) => item.state === 'ready')
|
||||
|
||||
return (
|
||||
<section className="workbench-status-strip" aria-label="Workbench readiness">
|
||||
<div className="status-strip-header">
|
||||
<div>
|
||||
<p className="eyebrow">V1 readiness</p>
|
||||
<p className="eyebrow">Platformstatus</p>
|
||||
<h2>
|
||||
<span className="sr-only">Workbench status: </span>
|
||||
{readyCount === items.length ? 'Mol workbench is ready' : 'Mol workbench needs attention'}
|
||||
{coreReady ? 'Kaartwerkruimte is gebruiksklaar' : 'Kaartwerkruimte vraagt aandacht'}
|
||||
</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>
|
||||
<span>onderdelen gereed</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="status-strip-grid">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Dispatch, FormEvent, SetStateAction } from 'react'
|
||||
import type { AreaRead, DatasetCreateResponse } from '../../types'
|
||||
import { getDatasetDisplayName } from '../../lib/datasetDisplay'
|
||||
|
||||
interface DatasetFormState {
|
||||
datasetType: string
|
||||
@@ -70,25 +71,25 @@ function normalizeDatasetRole(dataset: DatasetCreateResponse): 'reference' | 'ca
|
||||
|
||||
function datasetRoleLabel(role: string): string {
|
||||
if (role === 'reference') {
|
||||
return 'Reference'
|
||||
return 'Officiële referentie'
|
||||
}
|
||||
if (role === 'candidate') {
|
||||
return 'Candidate'
|
||||
return 'Analyse-resultaat'
|
||||
}
|
||||
return 'Source'
|
||||
return 'Eigen bron'
|
||||
}
|
||||
|
||||
function datasetActionHint(dataset: DatasetCreateResponse, role: string): string {
|
||||
if (role === 'reference') {
|
||||
return 'QA reference layer. Select it when comparing candidates against trusted geometry.'
|
||||
return 'Officiële laag voor kaartanalyse en kwaliteitscontrole.'
|
||||
}
|
||||
if (role === 'candidate') {
|
||||
return 'QA candidate layer. Open on the map or send to Export / QA.'
|
||||
return 'Bewaard resultaat dat opnieuw op de kaart, in kwaliteit of als download kan worden gebruikt.'
|
||||
}
|
||||
if (dataset.dataset_type === 'raster') {
|
||||
return 'Raster source. Use the raster tools in the inspector after selecting it.'
|
||||
return 'Luchtbeeld of rasterbron voor kaart- en beeldanalyse.'
|
||||
}
|
||||
return 'Source dataset. Inspect details before using it in downstream workflows.'
|
||||
return 'Ingeladen gegevensbron voor verdere analyse.'
|
||||
}
|
||||
|
||||
export function DatasetPanel({
|
||||
@@ -111,30 +112,30 @@ export function DatasetPanel({
|
||||
const roleSummaries = [
|
||||
{
|
||||
key: 'selected',
|
||||
label: 'Selected',
|
||||
label: 'Geselecteerd',
|
||||
count: selectedDatasetId ? 1 : 0,
|
||||
hint: 'Active',
|
||||
hint: 'Actief',
|
||||
className: 'dataset-role-selected',
|
||||
},
|
||||
{
|
||||
key: 'reference',
|
||||
label: 'Reference',
|
||||
label: 'Referentie',
|
||||
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'reference').length,
|
||||
hint: 'QA truth',
|
||||
hint: 'Officieel',
|
||||
className: 'dataset-role-reference',
|
||||
},
|
||||
{
|
||||
key: 'candidate',
|
||||
label: 'Candidate',
|
||||
label: 'Resultaat',
|
||||
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'candidate').length,
|
||||
hint: 'QA candidate',
|
||||
hint: 'Afgeleid',
|
||||
className: 'dataset-role-candidate',
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: 'Source',
|
||||
label: 'Eigen bron',
|
||||
count: datasets.filter((dataset) => normalizeDatasetRole(dataset) === 'source').length,
|
||||
hint: 'Uploads',
|
||||
hint: 'Ingeladen',
|
||||
className: 'dataset-role-source',
|
||||
},
|
||||
]
|
||||
@@ -143,23 +144,23 @@ export function DatasetPanel({
|
||||
<section className="workspace-panel" data-testid="dataset-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Data catalog</p>
|
||||
<h2>Datasets</h2>
|
||||
<p className="eyebrow">Ingeladen gegevens</p>
|
||||
<h2>Bronnen</h2>
|
||||
</div>
|
||||
<span className="count-pill">{readyDatasets}/{datasets.length} ready</span>
|
||||
<span className="count-pill">{readyDatasets}/{datasets.length} beschikbaar</span>
|
||||
</div>
|
||||
|
||||
<div className="data-selection-summary data-selection-summary-dataset">
|
||||
<span>Selected dataset</span>
|
||||
<strong>{selectedDataset?.name ?? 'No dataset selected'}</strong>
|
||||
<small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} / ${selectedDataset.status}` : 'Inspect a dataset to load details and tools.'}</small>
|
||||
<span>Actieve bron</span>
|
||||
<strong>{selectedDataset ? getDatasetDisplayName(selectedDataset) : 'Geen bron geselecteerd'}</strong>
|
||||
<small>{selectedDataset ? `${datasetRoleLabel(normalizeDatasetRole(selectedDataset))} · ${selectedDataset.status === 'ready' ? 'beschikbaar' : selectedDataset.status}` : 'Kies een bron om deze op de kaart te openen.'}</small>
|
||||
</div>
|
||||
|
||||
<details className="dataset-upload-block data-panel-form-block">
|
||||
<summary>Upload source data</summary>
|
||||
<summary>Eigen bronbestand toevoegen</summary>
|
||||
<form className="dataset-upload-form" onSubmit={onUploadDataset}>
|
||||
<label>
|
||||
Type
|
||||
Bestandstype
|
||||
<select
|
||||
value={datasetForm.datasetType}
|
||||
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, datasetType: event.target.value }))}
|
||||
@@ -170,7 +171,7 @@ export function DatasetPanel({
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Source
|
||||
Bronnaam
|
||||
<input
|
||||
value={datasetForm.source}
|
||||
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, source: event.target.value }))}
|
||||
@@ -178,12 +179,12 @@ export function DatasetPanel({
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
AOI
|
||||
Gebied
|
||||
<select
|
||||
value={datasetForm.areaId}
|
||||
onChange={(event) => onDatasetFormChange((previous) => ({ ...previous, areaId: event.target.value }))}
|
||||
>
|
||||
<option value="">No area</option>
|
||||
<option value="">Volledige bron</option>
|
||||
{areas.map((area) => (
|
||||
<option key={area.id} value={area.id}>
|
||||
{area.name}
|
||||
@@ -192,7 +193,7 @@ export function DatasetPanel({
|
||||
</select>
|
||||
</label>
|
||||
<label className="file-input-label">
|
||||
File
|
||||
Bestand
|
||||
<input
|
||||
type="file"
|
||||
accept=".geojson,.json,.tif,.tiff,.geotiff"
|
||||
@@ -200,7 +201,7 @@ export function DatasetPanel({
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-action" type="submit" disabled={!selectedProjectId}>
|
||||
Upload dataset
|
||||
Bron inladen
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
@@ -215,15 +216,15 @@ export function DatasetPanel({
|
||||
))}
|
||||
</div>
|
||||
|
||||
{loadingDatasets ? <p>Loading datasets...</p> : null}
|
||||
{loadingDatasets ? <p>Bronnen laden...</p> : null}
|
||||
{datasets.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<strong>No datasets yet</strong>
|
||||
<p>Upload vector, GeoJSON or raster data after selecting a project.</p>
|
||||
<strong>Nog geen bronnen beschikbaar</strong>
|
||||
<p>Voeg een vector-, GeoJSON- of rasterbestand toe aan deze werkruimte.</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="data-panel-list-block dataset-catalog-block">
|
||||
<p className="data-section-label">Available datasets</p>
|
||||
<p className="data-section-label">Beschikbare bronnen</p>
|
||||
<ul className="dataset-list">
|
||||
{datasets.map((dataset) => {
|
||||
const datasetRole = normalizeDatasetRole(dataset)
|
||||
@@ -236,22 +237,16 @@ export function DatasetPanel({
|
||||
<span className={`dataset-role-badge dataset-role-${datasetRole}`}>{roleLabel}</span>
|
||||
<span>{dataset.dataset_type}</span>
|
||||
</div>
|
||||
<strong className="dataset-card-title">{dataset.name}</strong>
|
||||
<div className="entity-meta">
|
||||
<span>Source: {dataset.source_name ?? dataset.source}</span>
|
||||
{dataset.reference_layer_name ? <span>Layer: {dataset.reference_layer_name}</span> : null}
|
||||
<span>CRS: {dataset.crs ?? dataset.vector_summary?.crs ?? 'n/a'}</span>
|
||||
<strong className="dataset-card-title">{getDatasetDisplayName(dataset)}</strong>
|
||||
<div className="entity-meta dataset-card-primary-meta">
|
||||
<span>{(dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 0).toLocaleString('nl-BE')} objecten</span>
|
||||
<span>{dataset.source_name ?? dataset.source}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className={dataset.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
|
||||
{dataset.status === 'ready' ? 'beschikbaar' : dataset.status === 'failed' ? 'mislukt' : 'bezig'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="dataset-metrics data-density-grid">
|
||||
<span>size: {formatBytes(dataset.size_bytes)}</span>
|
||||
<span>features: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'n/a'}</span>
|
||||
<span>bbox: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</span>
|
||||
</div>
|
||||
<p className="dataset-action-hint">{datasetActionHint(dataset, datasetRole)}</p>
|
||||
<div className="dataset-action-grid">
|
||||
<button
|
||||
@@ -261,17 +256,17 @@ export function DatasetPanel({
|
||||
onClick={() => onLoadDatasetDetails(selectedProjectId ?? '', dataset)}
|
||||
data-testid={`dataset-select-${dataset.id}`}
|
||||
>
|
||||
<span>Inspect</span>
|
||||
<small>Select / details</small>
|
||||
<span>Bekijken</span>
|
||||
<small>Details en acties</small>
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action dataset-action-button"
|
||||
type="button"
|
||||
onClick={() => onOpenDatasetInMap(dataset)}
|
||||
aria-label="Open in map"
|
||||
aria-label="Open op kaart"
|
||||
>
|
||||
<span>Map</span>
|
||||
<small>Open layer</small>
|
||||
<span>Kaart</span>
|
||||
<small>Laag openen</small>
|
||||
</button>
|
||||
<button
|
||||
className="secondary-action dataset-action-button"
|
||||
@@ -279,20 +274,33 @@ export function DatasetPanel({
|
||||
onClick={() => onOpenDatasetExport(dataset)}
|
||||
disabled={!(dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson')}
|
||||
>
|
||||
<span>Export / QA</span>
|
||||
<small>{dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson' ? 'Compare or export' : 'Vector/GeoJSON only'}</small>
|
||||
<span>Downloaden</span>
|
||||
<small>{dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson' ? 'GeoJSON of rapport' : 'Enkel vectorlagen'}</small>
|
||||
</button>
|
||||
</div>
|
||||
<details className="dataset-technical-details">
|
||||
<summary>Broninformatie</summary>
|
||||
<div className="entity-meta">
|
||||
<span>Bestand: {dataset.name}</span>
|
||||
<span>Bron: {dataset.source_name ?? dataset.source}</span>
|
||||
{dataset.reference_layer_name ? <span>Laag: {dataset.reference_layer_name}</span> : null}
|
||||
<span>CRS: {dataset.crs ?? dataset.vector_summary?.crs ?? 'onbekend'}</span>
|
||||
</div>
|
||||
<div className="dataset-metrics data-density-grid">
|
||||
<span>Grootte: {formatBytes(dataset.size_bytes)}</span>
|
||||
<span>Objecten: {dataset.feature_count ?? dataset.vector_summary?.feature_count ?? 'onbekend'}</span>
|
||||
<span>Begrenzing: {formatBounds(dataset.bounds_json ?? dataset.vector_summary?.bounds_json)}</span>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-action dataset-action-button"
|
||||
className="secondary-action"
|
||||
type="button"
|
||||
onClick={() => onRefreshMetadata(dataset.id)}
|
||||
disabled={dataset.dataset_type === 'raster'}
|
||||
aria-label="Refresh metadata"
|
||||
aria-label="Bronmetadata vernieuwen"
|
||||
>
|
||||
<span>Metadata</span>
|
||||
<small>{dataset.dataset_type === 'raster' ? 'Raster metadata is automatic' : 'Refresh vector stats'}</small>
|
||||
Metadata vernieuwen
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -145,6 +145,14 @@ export function DetectionLab({
|
||||
}: DetectionLabProps): JSX.Element {
|
||||
const selectedDetectionModel = detectionModels.find((model) => model.model_id === selectedDetectionModelId) ?? null
|
||||
const selectedModelAsset = modelAssets.find((asset) => asset.model_asset_id === selectedModelAssetId) ?? null
|
||||
const selectedOperatorProfile = DETECTION_OPERATOR_PROFILES.find(
|
||||
(profile) => profile.modelAssetId === selectedModelAssetId,
|
||||
) ?? null
|
||||
const yoloRuntimeReady = Boolean(
|
||||
yoloPreflight?.checks.enabled &&
|
||||
yoloPreflight.checks.dependencies_available &&
|
||||
yoloPreflight.checks.model_file_exists,
|
||||
)
|
||||
const detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'
|
||||
const detectionHasDataset = selectedDetectionDatasetId.length > 0
|
||||
const detectionHasModel = selectedDetectionModel !== null
|
||||
@@ -178,19 +186,19 @@ export function DetectionLab({
|
||||
detectionHasExplicitModelAsset &&
|
||||
detectionHasTileManifest
|
||||
const detectionRunBlockedReason = !selectedProjectId
|
||||
? 'Select or create a project first'
|
||||
? 'De regionale werkruimte is nog niet geladen'
|
||||
: !detectionHasDataset
|
||||
? 'Select a raster dataset'
|
||||
? 'Kies een luchtbeeld'
|
||||
: !detectionHasModel
|
||||
? 'Select a detection model'
|
||||
? 'Kies een analysemodel'
|
||||
: selectedDetectionModelId === 'manual-fixture-detector'
|
||||
? 'Fixture model is explicit test/demo-only'
|
||||
? 'Het fixturemodel is alleen bedoeld voor expliciete tests en demo\'s'
|
||||
: !detectionModelReady
|
||||
? selectedDetectionModel?.limitation_message ?? 'Selected model is not configured'
|
||||
? selectedDetectionModel?.limitation_message ?? 'Het gekozen model is niet geconfigureerd'
|
||||
: !detectionHasExplicitModelAsset
|
||||
? 'Select a local model asset deliberately'
|
||||
? 'Kies een lokaal modelbestand onder beheer'
|
||||
: !detectionHasTileManifest
|
||||
? 'Provide a raster tile manifest for configured YOLO'
|
||||
? 'Maak eerst beeldtegels voor het gekozen luchtbeeld'
|
||||
: null
|
||||
const calibrationRunReady = detectionRunReady && detectionReferenceDatasetId.length > 0 && calibrationThresholdText.trim().length > 0
|
||||
|
||||
@@ -198,18 +206,41 @@ export function DetectionLab({
|
||||
<section className="workspace-panel ai-lab-shell detection-lab-shell">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Object detection</p>
|
||||
<h2>Detection Lab</h2>
|
||||
<p className="eyebrow">Analyse van luchtbeelden</p>
|
||||
<h2>Gebouwen herkennen</h2>
|
||||
</div>
|
||||
<button className="secondary-action" type="button" onClick={onLoadModels} disabled={loadingDetectionModels}>
|
||||
Refresh models
|
||||
Status vernieuwen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="ai-user-summary" aria-label="Status gebouwdetectie">
|
||||
<div className="ai-user-summary-card ai-user-summary-card-primary">
|
||||
<span>Actieve analyse</span>
|
||||
<strong>Gebouwdetectie</strong>
|
||||
<p>{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Lokaal YOLO-model'}</p>
|
||||
</div>
|
||||
<div className={yoloRuntimeReady ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
|
||||
<span>PyTorch-runtime</span>
|
||||
<strong>{yoloRuntimeReady ? 'Gereed' : loadingDetectionModels ? 'Controleren...' : 'Niet gereed'}</strong>
|
||||
<p>{yoloRuntimeReady ? `${yoloPreflight?.runtime.cuda_available ? 'GPU' : 'CPU'} · lokaal model gevonden` : 'Controleer de modelconfiguratie onder beheer.'}</p>
|
||||
</div>
|
||||
<div className="ai-user-summary-card">
|
||||
<span>Gevalideerde kwaliteit</span>
|
||||
<strong>{selectedOperatorProfile ? `F1 ${selectedOperatorProfile.f1.toFixed(3)}` : 'Nog niet gekoppeld'}</strong>
|
||||
<p>{selectedOperatorProfile ? `${selectedOperatorProfile.positiveSampleCount} testgebieden · resultaten blijven controleplichtig` : 'Kies het goedgekeurde lokale profiel.'}</p>
|
||||
</div>
|
||||
<div className={rasterDatasets.length > 0 ? 'ai-user-summary-card ai-user-summary-card-ready' : 'ai-user-summary-card'}>
|
||||
<span>Beschikbare luchtbeelden</span>
|
||||
<strong>{rasterDatasets.length}</strong>
|
||||
<p>{rasterDatasets.length > 0 ? 'Klaar om een beeld te kiezen.' : 'Laad eerst een gegeorefereerd luchtbeeld in.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details className="ai-lab-model-surface" aria-label="Detection model capabilities">
|
||||
<summary>
|
||||
<span>Model registry</span>
|
||||
<strong>{detectionModels.length} models</strong>
|
||||
<span>Technische modelinformatie</span>
|
||||
<strong>{detectionModels.length} registraties</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-state-stack">
|
||||
@@ -257,21 +288,25 @@ export function DetectionLab({
|
||||
</details>
|
||||
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<div className="ai-lab-model-surface" aria-label="Local model asset selection">
|
||||
<details className="ai-lab-model-surface" aria-label="Local model asset selection">
|
||||
<summary>
|
||||
<span>Modelkeuze voor beheerders</span>
|
||||
<strong>{selectedOperatorProfile?.displayName ?? selectedModelAsset?.display_name ?? 'Geen lokaal model'}</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Explicit model asset</h3>
|
||||
<p>Local model assets are read-only. No model file is selected automatically. Choose an existing local model asset before submitting configured YOLO.</p>
|
||||
<h3>Lokaal modelbestand</h3>
|
||||
<p>GeoIntel kiest automatisch het actieve lokale model. Een beheerder kan hier bewust een ander reeds aanwezig, alleen-lezen modelbestand kiezen.</p>
|
||||
</div>
|
||||
<span className={selectedModelAsset ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{selectedModelAsset ? 'asset selected' : 'no explicit asset'}
|
||||
{selectedModelAsset ? 'model gekozen' : 'geen model gekozen'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="model-asset-guidance">
|
||||
<strong>Operator profiles</strong>
|
||||
<strong>Gevalideerde profielen</strong>
|
||||
<p>
|
||||
Candidate profiles apply a local model asset and confidence threshold only after an explicit click.
|
||||
Promoted profiles still require explicit operator action and do not mutate the runtime environment.
|
||||
Een profiel koppelt een lokaal model aan een gemeten zekerheidsdrempel. Een andere keuze geldt alleen voor de huidige analyse en wijzigt de serverconfiguratie niet.
|
||||
</p>
|
||||
</div>
|
||||
<div className="operator-profile-grid" aria-label="Configured YOLO operator profiles">
|
||||
@@ -288,22 +323,22 @@ export function DetectionLab({
|
||||
<div className="operator-profile-card-header">
|
||||
<strong>{profile.displayName}</strong>
|
||||
<span className={profile.defaultApproved ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{profile.defaultApproved ? 'default-approved' : 'Candidate only - not default-approved'}
|
||||
{profile.defaultApproved ? 'standaardprofiel' : 'kandidaat · extra controle vereist'}
|
||||
</span>
|
||||
</div>
|
||||
<p>{profile.description}</p>
|
||||
<div className="operator-profile-metrics">
|
||||
<span>threshold {profile.confidenceThreshold.toFixed(2)}</span>
|
||||
<span>drempel {profile.confidenceThreshold.toFixed(2)}</span>
|
||||
<span>precision {profile.precision.toFixed(3)}</span>
|
||||
<span>recall {profile.recall.toFixed(3)}</span>
|
||||
<span>F1 {profile.f1.toFixed(3)}</span>
|
||||
<span>positive AOIs {profile.positiveSampleCount}</span>
|
||||
<span>max background FP {profile.maxBackgroundDetections}</span>
|
||||
<span>testgebieden {profile.positiveSampleCount}</span>
|
||||
<span>max. achtergrondfouten {profile.maxBackgroundDetections}</span>
|
||||
</div>
|
||||
<div className="entity-meta">
|
||||
<span>asset: {profile.modelAssetId}</span>
|
||||
<span>promotionRecommendation: {profile.promotionRecommendation}</span>
|
||||
<span>available: {profileAsset ? 'yes' : 'not mounted'}</span>
|
||||
<span>modelbestand: {profile.modelAssetId}</span>
|
||||
<span>beoordeling: {profile.promotionRecommendation}</span>
|
||||
<span>beschikbaar: {profileAsset ? 'ja' : 'niet gekoppeld'}</span>
|
||||
</div>
|
||||
<p className="field-guidance">{profile.limitationMessage}</p>
|
||||
<button
|
||||
@@ -312,7 +347,7 @@ export function DetectionLab({
|
||||
onClick={() => onApplyOperatorProfile(profile)}
|
||||
disabled={!profileAsset}
|
||||
>
|
||||
Apply profile
|
||||
Profiel gebruiken
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -320,17 +355,16 @@ export function DetectionLab({
|
||||
</div>
|
||||
{selectedModelAsset ? (
|
||||
<div className="model-asset-guidance">
|
||||
<strong>Selected model asset status</strong>
|
||||
<strong>Status gekozen model</strong>
|
||||
<p>
|
||||
{selectedModelAsset.display_name} is selected for this browser-run request. Runtime default activation
|
||||
remains a separate guarded operator action backed by a promotion report.
|
||||
{selectedModelAsset.display_name} wordt voor deze analyse gebruikt. De standaard serverconfiguratie blijft ongewijzigd.
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<label>
|
||||
Local model file
|
||||
Lokaal modelbestand
|
||||
<select value={selectedModelAssetId} onChange={(event) => onSelectModelAsset(event.target.value)}>
|
||||
<option value="">Select explicit local model asset</option>
|
||||
<option value="">Kies een lokaal modelbestand</option>
|
||||
{modelAssets.map((asset) => (
|
||||
<option key={asset.model_asset_id} value={asset.model_asset_id}>
|
||||
{asset.display_name} {asset.active ? '(active)' : ''}
|
||||
@@ -340,8 +374,8 @@ export function DetectionLab({
|
||||
</label>
|
||||
{modelAssets.length === 0 && !loadingDetectionModels ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No local model assets found.</strong>
|
||||
<p>Mount model files into the backend model directory or continue with the configured YOLO_MODEL_PATH.</p>
|
||||
<strong>Geen lokaal modelbestand gevonden.</strong>
|
||||
<p>Plaats een gecontroleerd model in de modelmap of configureer het bestaande YOLO-modelpad.</p>
|
||||
</div>
|
||||
) : null}
|
||||
{selectedModelAsset ? (
|
||||
@@ -356,13 +390,14 @@ export function DetectionLab({
|
||||
<p>{selectedModelAsset.limitation_message}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
|
||||
<details className="ai-lab-model-surface" aria-label="YOLO runtime preflight">
|
||||
<summary>
|
||||
<span>YOLO runtime preflight</span>
|
||||
<strong>{yoloPreflight?.status ?? 'not loaded'}</strong>
|
||||
<span>Technische runtimecontrole</span>
|
||||
<strong>{yoloRuntimeReady ? 'gereed' : yoloPreflight?.status ?? 'niet geladen'}</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-section-header">
|
||||
@@ -447,72 +482,72 @@ export function DetectionLab({
|
||||
|
||||
<div className="lab-block">
|
||||
<div className="ai-lab-run-surface" aria-label="Detection run controls">
|
||||
<h3>Run detection</h3>
|
||||
<h3>Nieuwe beeldanalyse</h3>
|
||||
<div
|
||||
className={detectionRunReady ? 'lab-readiness-panel lab-readiness-panel-ready' : 'lab-readiness-panel'}
|
||||
aria-label="Detection run readiness"
|
||||
>
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Run readiness</h3>
|
||||
<p>Checks the selected dataset, model and tile manifest before submitting a detection job.</p>
|
||||
<h3>Wat is nog nodig?</h3>
|
||||
<p>De analyse start zodra een luchtbeeld en de bijbehorende beeldtegels beschikbaar zijn.</p>
|
||||
</div>
|
||||
<span className={detectionRunReady ? 'status-badge status-badge-ready' : 'status-badge'}>
|
||||
{detectionRunReady ? 'Ready to submit' : 'Blocked'}
|
||||
{detectionRunReady ? 'Klaar om te starten' : 'Nog niet startklaar'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="lab-readiness-grid">
|
||||
<div className={detectionHasDataset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||
<span>Raster dataset</span>
|
||||
<strong>{detectionHasDataset ? 'Selected' : 'Select a raster dataset'}</strong>
|
||||
<span>Luchtbeeld</span>
|
||||
<strong>{detectionHasDataset ? 'Geselecteerd' : 'Nog geen luchtbeeld'}</strong>
|
||||
</div>
|
||||
<div className={detectionModelReady ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||
<span>Model availability</span>
|
||||
<span>Analysemodel</span>
|
||||
<strong>
|
||||
{detectionModelReady
|
||||
? `${selectedDetectionModel?.display_name ?? selectedDetectionModelId} is configured`
|
||||
: selectedDetectionModel?.limitation_message ?? 'Select a configured model'}
|
||||
? 'Lokaal gebouwmodel beschikbaar'
|
||||
: 'Model niet beschikbaar'}
|
||||
</strong>
|
||||
</div>
|
||||
<div className={detectionHasTileManifest ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||
<span>Tile manifest</span>
|
||||
<span>Beeldtegels</span>
|
||||
<strong>
|
||||
{detectionRequiresTileManifest
|
||||
? detectionHasTileManifest
|
||||
? 'Provided for configured YOLO'
|
||||
: 'Required for configured YOLO'
|
||||
: 'Not required for this model'}
|
||||
? 'Beschikbaar'
|
||||
: 'Maak eerst tegels vanuit het luchtbeeld'
|
||||
: 'Niet vereist'}
|
||||
</strong>
|
||||
</div>
|
||||
<div className={detectionHasExplicitModelAsset ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||
<span>Local model asset</span>
|
||||
<span>Lokale modelkeuze</span>
|
||||
<strong>
|
||||
{selectedDetectionModelId === 'yolo-configured'
|
||||
? selectedModelAsset
|
||||
? selectedModelAsset.display_name
|
||||
? selectedOperatorProfile?.displayName ?? selectedModelAsset.display_name
|
||||
: modelAssets.length > 0
|
||||
? 'Select a local model asset deliberately'
|
||||
: 'No local assets reported; backend configured path only'
|
||||
: 'Not required for this model'}
|
||||
? 'Kies een lokaal model onder beheer'
|
||||
: 'Geen lokaal model gevonden'
|
||||
: 'Niet vereist'}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={detectionRunReady ? 'lab-action-guardrail lab-action-guardrail-ready' : 'lab-action-guardrail'}>
|
||||
<span>Run action</span>
|
||||
<strong>{detectionRunReady ? 'Ready to submit a detection job' : detectionRunBlockedReason}</strong>
|
||||
<span>Analyse</span>
|
||||
<strong>{detectionRunReady ? 'Klaar om gebouwen te zoeken' : detectionRunBlockedReason}</strong>
|
||||
</div>
|
||||
{rasterDatasets.length === 0 ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No raster datasets available for detection.</strong>
|
||||
<p>Upload or select a raster dataset in Data before running object detection.</p>
|
||||
<strong>Geen luchtbeeld beschikbaar in deze werkruimte.</strong>
|
||||
<p>Voeg onder Bronnen een gegeorefereerde GeoTIFF toe. Daarna kan GeoIntel er beeldtegels en een detectierun van maken.</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="lab-form-grid">
|
||||
<label>
|
||||
Raster dataset
|
||||
Luchtbeeld
|
||||
<select value={selectedDetectionDatasetId} onChange={(event) => onSelectDataset(event.target.value)}>
|
||||
<option value="">Select raster dataset</option>
|
||||
<option value="">Kies een luchtbeeld</option>
|
||||
{rasterDatasets.map((dataset) => (
|
||||
<option key={dataset.id} value={dataset.id}>
|
||||
{dataset.name}
|
||||
@@ -521,7 +556,7 @@ export function DetectionLab({
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Model
|
||||
Analysemodel
|
||||
<select value={selectedDetectionModelId} onChange={(event) => onSelectModel(event.target.value)}>
|
||||
{detectionModels.map((model) => (
|
||||
<option key={model.model_id} value={model.model_id}>
|
||||
@@ -531,7 +566,7 @@ export function DetectionLab({
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Min confidence
|
||||
Minimale zekerheid
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -542,14 +577,14 @@ export function DetectionLab({
|
||||
/>
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<span className="field-guidance">
|
||||
Use a validated operator profile, or enter a threshold manually for calibration.
|
||||
Het actieve gevalideerde profiel gebruikt standaard 0,15 voor een evenwichtige gebouwcontrole.
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
</div>
|
||||
{selectedDetectionModelId === 'yolo-configured' ? (
|
||||
<label>
|
||||
Tile manifest
|
||||
Beeldtegelbestand
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Raster tile manifest path"
|
||||
@@ -560,13 +595,13 @@ export function DetectionLab({
|
||||
) : null}
|
||||
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
|
||||
<div className="linked-manifest-card">
|
||||
<strong>Linked tile manifest</strong>
|
||||
<strong>Gekoppelde beeldtegels</strong>
|
||||
<p>{detectionTileManifestPath}</p>
|
||||
<span>Refresh preflight after changing model asset or manifest path.</span>
|
||||
<span>De technische controle wordt vernieuwd wanneer het model of tegelbestand wijzigt.</span>
|
||||
</div>
|
||||
) : null}
|
||||
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||
Run detection
|
||||
Zoek gebouwen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -590,7 +625,12 @@ export function DetectionLab({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="ai-lab-run-surface guided-calibration-surface" aria-label="Guided calibration runner">
|
||||
<details className="ai-lab-model-surface guided-calibration-surface" aria-label="Guided calibration runner">
|
||||
<summary>
|
||||
<span>Modelkalibratie voor beheerders</span>
|
||||
<strong>{detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}</strong>
|
||||
</summary>
|
||||
<div className="ai-lab-disclosure-body">
|
||||
<div className="ai-lab-section-header">
|
||||
<div>
|
||||
<h3>Guided calibration runner</h3>
|
||||
@@ -705,23 +745,24 @@ export function DetectionLab({
|
||||
<p>Choose a reference dataset and threshold set, then start the explicit sweep.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div className="ai-lab-results-surface" aria-label="Detection results">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h3>Detection results</h3>
|
||||
<p className="muted">Load persisted detections and filter by class or confidence.</p>
|
||||
<h3>Gevonden objecten</h3>
|
||||
<p className="muted">Bekijk eerder bewaarde analyses en filter op type of zekerheid.</p>
|
||||
</div>
|
||||
<button className="secondary-action" type="button" onClick={onLoadRuns} disabled={!selectedProjectId}>
|
||||
Refresh runs
|
||||
Analyses vernieuwen
|
||||
</button>
|
||||
</div>
|
||||
<div className="lab-form-grid">
|
||||
<label>
|
||||
Run
|
||||
Analyse
|
||||
<select value={selectedDetectionRunId} onChange={(event) => onSelectRun(event.target.value)}>
|
||||
<option value="">Select detection run</option>
|
||||
<option value="">Kies een bewaarde analyse</option>
|
||||
{detectionRuns.map((run) => (
|
||||
<option key={run.id} value={run.id}>
|
||||
{run.model_name || 'detection'} - {run.status} - {run.id}
|
||||
@@ -730,16 +771,16 @@ export function DetectionLab({
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Class
|
||||
Type object
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Class filter"
|
||||
placeholder="bijvoorbeeld building"
|
||||
value={detectionClassFilter}
|
||||
onChange={(event) => onSetClassFilter(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Min confidence
|
||||
Minimale zekerheid
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
@@ -751,18 +792,18 @@ export function DetectionLab({
|
||||
</label>
|
||||
</div>
|
||||
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
|
||||
Load detections
|
||||
Resultaten laden
|
||||
</button>
|
||||
{loadingDetectionResults ? (
|
||||
<div className="result-state result-state-loading">
|
||||
<strong>Loading detection results.</strong>
|
||||
<p>Retrieving persisted detections for the selected run.</p>
|
||||
<strong>Resultaten laden.</strong>
|
||||
<p>De bewaarde objecten van deze analyse worden opgehaald.</p>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="ai-lab-state-stack">
|
||||
<div className="result-state result-state-ready">
|
||||
<strong>Detections loaded: {detectionItems.length}</strong>
|
||||
<p>{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}</p>
|
||||
<strong>{detectionItems.length} objecten geladen</strong>
|
||||
<p>{selectedDetectionRunId ? 'Deze resultaten zijn bewaard in de database.' : 'Kies eerst een bewaarde analyse.'}</p>
|
||||
</div>
|
||||
</div>
|
||||
{detectionItems.length > 0 ? (
|
||||
|
||||
@@ -98,44 +98,45 @@ export function ExportCenter({
|
||||
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
|
||||
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
|
||||
const handoffStatus = selectedProjectId ? 'project selected' : 'select project'
|
||||
const handoffStatus = selectedProjectId ? 'werkruimte gekozen' : 'kies een werkruimte'
|
||||
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
|
||||
const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports])
|
||||
const latestHandoffArtifacts = useMemo(
|
||||
() => [
|
||||
{
|
||||
key: 'project_report_html',
|
||||
label: 'Project report',
|
||||
detail: 'HTML report for human review',
|
||||
label: 'Leesbaar rapport',
|
||||
detail: 'HTML-rapport om te bekijken en te delen',
|
||||
item: getLatestExportByType(exports, 'project_report_html'),
|
||||
},
|
||||
{
|
||||
key: 'project_metadata_json',
|
||||
label: 'Project metadata',
|
||||
detail: 'Machine-readable project inventory',
|
||||
label: 'Werkruimtedata',
|
||||
detail: 'Technische inventaris in JSON-formaat',
|
||||
item: getLatestExportByType(exports, 'project_metadata_json'),
|
||||
},
|
||||
{
|
||||
key: 'dataset_geojson',
|
||||
label: 'Dataset GeoJSON',
|
||||
detail: 'Selected vector/reference layer',
|
||||
label: 'Kaartlaag (GeoJSON)',
|
||||
detail: 'De geselecteerde vector- of referentielaag',
|
||||
item: getLatestExportByType(exports, 'dataset_geojson'),
|
||||
},
|
||||
{
|
||||
key: 'detection_geojson',
|
||||
label: 'Detection GeoJSON',
|
||||
detail: 'Persisted detection geometries',
|
||||
label: 'Herkende gebouwen (GeoJSON)',
|
||||
detail: 'Bewaarde geometrieën uit beeldanalyse',
|
||||
item: getLatestExportByType(exports, 'detection_geojson'),
|
||||
},
|
||||
{
|
||||
key: 'segmentation_geojson',
|
||||
label: 'Segmentation GeoJSON',
|
||||
detail: 'Persisted segmentation polygons',
|
||||
label: 'Segmentaties (GeoJSON)',
|
||||
detail: 'Bewaarde segmentatiepolygonen',
|
||||
item: getLatestExportByType(exports, 'segmentation_geojson'),
|
||||
},
|
||||
],
|
||||
[exports],
|
||||
)
|
||||
const availableLatestArtifacts = latestHandoffArtifacts.filter((artifact) => artifact.item)
|
||||
const filteredExports = useMemo(
|
||||
() =>
|
||||
exports.filter((item) => {
|
||||
@@ -154,10 +155,10 @@ export function ExportCenter({
|
||||
<div className="export-center-shell">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h2>Export Center</h2>
|
||||
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
|
||||
<h2>Downloads en rapporten</h2>
|
||||
<p className="muted">Bewaar de geselecteerde kaartlaag, analyseresultaten of een leesbaar projectrapport.</p>
|
||||
</div>
|
||||
<span className="count-pill">{exports.length} exports</span>
|
||||
<span className="count-pill">{exports.length} bestanden</span>
|
||||
</div>
|
||||
|
||||
<div className="export-summary-surface" aria-label="Export summary">
|
||||
@@ -167,12 +168,12 @@ export function ExportCenter({
|
||||
<strong>{geojsonExports}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Reports</span>
|
||||
<span>Rapporten</span>
|
||||
<strong>{reportExports}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Selected dataset</span>
|
||||
<strong>{selectedDataset?.name ?? 'none'}</strong>
|
||||
<span>Geselecteerde kaartlaag</span>
|
||||
<strong>{selectedDataset?.name ?? 'geen'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,20 +189,20 @@ export function ExportCenter({
|
||||
</div>
|
||||
<div className="handoff-readiness-grid">
|
||||
<div>
|
||||
<span>Vector dataset</span>
|
||||
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong>
|
||||
<span>Vectorkaartlaag</span>
|
||||
<strong>{canExportDataset ? selectedDataset?.name : 'geen beschikbaar'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Detection run</span>
|
||||
<strong>{selectedDetectionRunId || 'none'}</strong>
|
||||
<span>Gebouwanalyse</span>
|
||||
<strong>{selectedDetectionRunId || 'geen'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Segmentation run</span>
|
||||
<strong>{selectedSegmentationRunId || 'none'}</strong>
|
||||
<span>Segmentatieanalyse</span>
|
||||
<strong>{selectedSegmentationRunId || 'geen'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest handoff artifact</span>
|
||||
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'none'}</strong>
|
||||
<span>Laatste bestand</span>
|
||||
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'geen'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -209,16 +210,16 @@ export function ExportCenter({
|
||||
|
||||
<div className="latest-artifact-surface" aria-label="Latest handoff artifacts">
|
||||
<div>
|
||||
<h3>Latest handoff artifacts</h3>
|
||||
<p className="muted">Open the newest artifact of each handoff type without searching the full export history.</p>
|
||||
<h3>Laatste downloads</h3>
|
||||
<p className="muted">Open de recentste bestanden zonder in de volledige geschiedenis te zoeken.</p>
|
||||
</div>
|
||||
<div className="latest-artifact-grid">
|
||||
{latestHandoffArtifacts.map((artifact) => {
|
||||
{availableLatestArtifacts.map((artifact) => {
|
||||
const item = artifact.item
|
||||
return (
|
||||
<div className={item ? 'latest-artifact-card' : 'latest-artifact-card latest-artifact-card-empty'} key={artifact.key}>
|
||||
<span>{artifact.label}</span>
|
||||
<strong>{item ? item.status : 'No artifact yet'}</strong>
|
||||
<strong>{item ? (item.status === 'ready' ? 'Beschikbaar' : item.status) : 'Nog niet gemaakt'}</strong>
|
||||
<p>{item ? compactPath(item.storage_path) : artifact.detail}</p>
|
||||
{item ? (
|
||||
<>
|
||||
@@ -226,13 +227,13 @@ export function ExportCenter({
|
||||
<div className="latest-artifact-actions">
|
||||
{canPreviewJson(item) ? (
|
||||
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
|
||||
Preview
|
||||
Bekijken
|
||||
</button>
|
||||
) : (
|
||||
<span className="status-badge">download-only</span>
|
||||
<span className="status-badge">alleen downloaden</span>
|
||||
)}
|
||||
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
|
||||
Download
|
||||
Downloaden
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
@@ -240,19 +241,25 @@ export function ExportCenter({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{availableLatestArtifacts.length === 0 ? (
|
||||
<div className="result-state result-state-empty latest-download-empty">
|
||||
<strong>Nog geen downloads gemaakt</strong>
|
||||
<p>Kies hieronder een rapport of de actieve kaartlaag.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="export-actions-surface" aria-label="Export artifact actions">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h3>Artifact actions</h3>
|
||||
<p className="muted">Create review artifacts from the current project, dataset or selected AI analysis run.</p>
|
||||
<h3>Wat wil je bewaren?</h3>
|
||||
<p className="muted">Maak een leesbaar rapport of download gegevens voor verder gebruik.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="export-action-grid">
|
||||
<div className="handoff-action-grid">
|
||||
<div className="handoff-action-card">
|
||||
<div className="handoff-action-card handoff-action-card-technical">
|
||||
<span>Refresh</span>
|
||||
<strong>Export registry</strong>
|
||||
<p>Reload the persisted artifact list for the selected project.</p>
|
||||
@@ -267,17 +274,17 @@ export function ExportCenter({
|
||||
</button>
|
||||
</div>
|
||||
<div className="handoff-action-card handoff-action-card-primary">
|
||||
<span>Project report</span>
|
||||
<strong>HTML summary</strong>
|
||||
<p>Generate the human-readable V1 handoff report from persisted project state.</p>
|
||||
<span>Rapport</span>
|
||||
<strong>Leesbaar projectoverzicht</strong>
|
||||
<p>Maak een HTML-rapport met de beschikbare bronnen, controles en resultaten.</p>
|
||||
<button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
||||
Export project report HTML
|
||||
Projectrapport maken
|
||||
</button>
|
||||
</div>
|
||||
<div className="handoff-action-card">
|
||||
<span>Metadata</span>
|
||||
<strong>Project JSON</strong>
|
||||
<p>Export project, dataset, QA and artifact metadata for machine-readable review.</p>
|
||||
<span>Technische gegevens</span>
|
||||
<strong>Projectgegevens (JSON)</strong>
|
||||
<p>Download de volledige projectinventaris voor geautomatiseerde verwerking.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-action"
|
||||
@@ -285,33 +292,33 @@ export function ExportCenter({
|
||||
disabled={!selectedProjectId || exporting}
|
||||
data-testid="export-project-metadata"
|
||||
>
|
||||
Export project metadata JSON
|
||||
Projectgegevens downloaden
|
||||
</button>
|
||||
</div>
|
||||
<div className="handoff-action-card">
|
||||
<span>Vector dataset</span>
|
||||
<strong>Selected GeoJSON</strong>
|
||||
<p>Export the active vector/reference dataset when it is selected and ready.</p>
|
||||
<span>Kaartlaag</span>
|
||||
<strong>Geselecteerde bron (GeoJSON)</strong>
|
||||
<p>Download de actieve vector- of referentielaag.</p>
|
||||
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
|
||||
Export selected vector GeoJSON
|
||||
Kaartlaag downloaden
|
||||
</button>
|
||||
</div>
|
||||
<div className="handoff-action-card">
|
||||
{selectedDetectionRunId ? <div className="handoff-action-card">
|
||||
<span>Detection run</span>
|
||||
<strong>Detections GeoJSON</strong>
|
||||
<p>Export persisted detection geometries for the selected analysis run.</p>
|
||||
<button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
|
||||
Export selected detection run GeoJSON
|
||||
</button>
|
||||
</div>
|
||||
<div className="handoff-action-card">
|
||||
</div> : null}
|
||||
{selectedSegmentationRunId ? <div className="handoff-action-card">
|
||||
<span>Segmentation run</span>
|
||||
<strong>Segmentations GeoJSON</strong>
|
||||
<p>Export persisted segmentation polygons for the selected analysis run.</p>
|
||||
<button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
|
||||
Export selected segmentation run GeoJSON
|
||||
</button>
|
||||
</div>
|
||||
</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -344,7 +351,12 @@ export function ExportCenter({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="export-history-surface" aria-label="Export history">
|
||||
<details className="export-history-surface export-history-disclosure" aria-label="Export history">
|
||||
<summary>
|
||||
<span>Downloadgeschiedenis</span>
|
||||
<strong>{exports.length} bestanden</strong>
|
||||
</summary>
|
||||
<div className="export-history-body">
|
||||
<div>
|
||||
<h3>Export history</h3>
|
||||
<p className="muted">Review persisted artifacts and open JSON previews where supported.</p>
|
||||
@@ -444,7 +456,8 @@ export function ExportCenter({
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AreaRead, DatasetCreateResponse, MapViewportState, ProjectRead, Qa
|
||||
import { featureCollectionBounds } from '../../lib/geojsonBounds'
|
||||
import { useMapThemeSelectionInsights } from '../../hooks/useMapThemeSelectionInsights'
|
||||
import { useTemporalComparison } from '../../hooks/useTemporalComparison'
|
||||
import { getDatasetDisplayName } from '../../lib/datasetDisplay'
|
||||
|
||||
const DEFAULT_SELECTED_FEATURE_FILENAME = 'selected-feature.geojson'
|
||||
const DEFAULT_AREA_SELECTION_FILENAME = 'area-selection.geojson'
|
||||
@@ -443,7 +444,6 @@ interface MapWorkspaceProps {
|
||||
availableMapDatasets: DatasetCreateResponse[]
|
||||
selectedMapDatasetId: string
|
||||
onSelectMapArea: (areaId: string) => void
|
||||
onSelectProject: (projectId: string) => void
|
||||
onOpenDatasetInMap: (dataset: DatasetCreateResponse) => void
|
||||
onSetAreaLayerVisible: (visible: boolean) => void
|
||||
onSetAreaLayerOpacity: (opacity: number) => void
|
||||
@@ -512,7 +512,6 @@ export function MapWorkspace({
|
||||
availableMapDatasets,
|
||||
selectedMapDatasetId,
|
||||
onSelectMapArea,
|
||||
onSelectProject,
|
||||
onOpenDatasetInMap,
|
||||
onSetAreaLayerVisible,
|
||||
onSetAreaLayerOpacity,
|
||||
@@ -590,16 +589,9 @@ export function MapWorkspace({
|
||||
const activeTheme = DATA_THEMES.find((theme) => theme.id === activeThemeId) ?? DATA_THEMES[0]
|
||||
const activeThemeMapStyle = DATA_THEME_MAP_STYLES[activeTheme.id]
|
||||
const activeThemeDataset = themeDatasetMap[activeTheme.id]
|
||||
const operationalScopeProjects = useMemo(() => {
|
||||
const scoped = projects.filter((project) => project.name === MOL_PROJECT_NAME || project.name === KEMPEN_PROJECT_NAME)
|
||||
const selected = projects.find((project) => project.id === selectedProjectId)
|
||||
if (selected && !scoped.some((project) => project.id === selected.id)) {
|
||||
return [selected, ...scoped]
|
||||
}
|
||||
return scoped
|
||||
}, [projects, selectedProjectId])
|
||||
const activeScopeProject = projects.find((project) => project.id === selectedProjectId) ?? null
|
||||
const activeScopeLabel = activeScopeProject ? operationalScopeProjectLabel(activeScopeProject) : 'Werkgebied'
|
||||
const municipalityAreaCount = areas.filter((area) => /^Gemeente\s/i.test(area.name)).length
|
||||
const activeTemporalSeriesGroups = useMemo(
|
||||
() => listThemeTemporalSeries(availableMapDatasets, activeTheme),
|
||||
[activeTheme, availableMapDatasets],
|
||||
@@ -964,19 +956,11 @@ export function MapWorkspace({
|
||||
<p>Dit zijn databronnen, geen AI-modellen.</p>
|
||||
</div>
|
||||
</div>
|
||||
<label className="geo-project-scope-select">
|
||||
Regio
|
||||
<select
|
||||
aria-label="Regio"
|
||||
value={selectedProjectId ?? ''}
|
||||
onChange={(event) => onSelectProject(event.target.value)}
|
||||
disabled={operationalScopeProjects.length < 2}
|
||||
>
|
||||
{operationalScopeProjects.map((project) => (
|
||||
<option key={project.id} value={project.id}>{operationalScopeProjectLabel(project)}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="geo-loaded-scope" aria-label="Ingeladen regiobereik">
|
||||
<span>Ingeladen bereik</span>
|
||||
<strong>{activeScopeLabel}</strong>
|
||||
<small>{municipalityAreaCount > 0 ? `${municipalityAreaCount} gemeenten en de volledige regio beschikbaar` : 'Regionale gegevens worden geladen'}</small>
|
||||
</div>
|
||||
<div className="geo-theme-list">
|
||||
{DATA_THEMES.map((theme) => {
|
||||
const dataset = themeDatasetMap[theme.id]
|
||||
@@ -1006,7 +990,7 @@ export function MapWorkspace({
|
||||
<strong>
|
||||
{analysisMode === 'evolution'
|
||||
? activeTemporalSeriesGroup?.label ?? 'Geen tijdreeks beschikbaar'
|
||||
: activeThemeDataset?.name ?? 'Geen databron beschikbaar'}
|
||||
: activeThemeDataset ? getDatasetDisplayName(activeThemeDataset) : 'Geen databron beschikbaar'}
|
||||
</strong>
|
||||
<small>
|
||||
{analysisMode === 'evolution'
|
||||
@@ -1065,9 +1049,8 @@ export function MapWorkspace({
|
||||
) : null}
|
||||
|
||||
<label className="geo-scope-select">
|
||||
Werkgebied
|
||||
Gemeente of volledige regio
|
||||
<select aria-label="Werkgebied" value={selectedMapAreaId} onChange={(event) => handleSelectMapArea(event.target.value)} disabled={areas.length === 0}>
|
||||
<option value="">Geen werkgebied</option>
|
||||
{areas.map((area) => (
|
||||
<option key={area.id} value={area.id}>{area.name}</option>
|
||||
))}
|
||||
@@ -1321,7 +1304,7 @@ export function MapWorkspace({
|
||||
{analysisMode === 'evolution'
|
||||
? activeTemporalSeriesGroup?.label ?? 'geen vergelijkbare tijdreeks'
|
||||
: activeThemeDataset
|
||||
? `${activeThemeDataset.source_name ?? activeThemeDataset.source} · ${activeThemeDataset.name}`
|
||||
? getDatasetDisplayName(activeThemeDataset)
|
||||
: 'niet beschikbaar'}
|
||||
</span>
|
||||
{usesDefaultOsmBasemap ? <span><strong>Ondergrond:</strong> OpenStreetMap</span> : null}
|
||||
|
||||
@@ -36,28 +36,28 @@ export function AreaPanel({
|
||||
<section data-testid="area-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Spatial scope</p>
|
||||
<h2>AOI manager</h2>
|
||||
<p className="eyebrow">Geografisch bereik</p>
|
||||
<h2>Gebieden</h2>
|
||||
</div>
|
||||
<span className="count-pill">{areas.length}</span>
|
||||
</div>
|
||||
<p className="muted">{selectedProject ? `Project: ${selectedProject.name}` : 'Select a project first'}</p>
|
||||
<p className="muted">{selectedProject ? selectedProject.name : 'Geen werkruimte actief'}</p>
|
||||
|
||||
<div className="data-selection-summary data-selection-summary-area">
|
||||
<span>Shown on map</span>
|
||||
<strong>{selectedArea?.name ?? 'No AOI selected'}</strong>
|
||||
<small>{selectedArea?.area_m2 ? `${selectedArea.area_m2.toFixed(2)} m2` : 'Select an AOI with geometry for spatial review.'}</small>
|
||||
<span>Getoond op de kaart</span>
|
||||
<strong>{selectedArea?.name ?? 'Geen gebied geselecteerd'}</strong>
|
||||
<small>{selectedArea?.area_m2 ? `${(selectedArea.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Kies een gemeente of de volledige regio op de kaart.'}</small>
|
||||
</div>
|
||||
|
||||
<details className="data-panel-form-block">
|
||||
<summary>Create spatial scope</summary>
|
||||
<summary>Eigen gebied toevoegen (GeoJSON)</summary>
|
||||
<form className="compact-form" onSubmit={onCreateArea}>
|
||||
<label>
|
||||
AOI name
|
||||
Naam
|
||||
<input
|
||||
value={areaForm.name}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, name: event.target.value })}
|
||||
placeholder="AOI name"
|
||||
placeholder="Naam van het gebied"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
@@ -69,7 +69,7 @@ export function AreaPanel({
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
GeoJSON geometry
|
||||
GeoJSON-geometrie
|
||||
<textarea
|
||||
value={areaForm.geometry}
|
||||
onChange={(event) => onUpdateAreaForm({ ...areaForm, geometry: event.target.value })}
|
||||
@@ -77,22 +77,22 @@ export function AreaPanel({
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-action" type="submit" disabled={!selectedProjectId}>
|
||||
Create area
|
||||
Gebied toevoegen
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
{loadingAreas ? <p>Loading areas...</p> : null}
|
||||
{areas.length === 0 ? <p>No areas yet</p> : null}
|
||||
<div className="data-panel-list-block">
|
||||
<p className="data-section-label">Saved AOIs</p>
|
||||
{loadingAreas ? <p>Gebieden laden...</p> : null}
|
||||
{areas.length === 0 ? <p>Nog geen gebieden beschikbaar.</p> : null}
|
||||
<details className="data-panel-list-block area-catalog-disclosure">
|
||||
<summary>{areas.length} beschikbare gemeenten en regiogrenzen</summary>
|
||||
<ul className="entity-list">
|
||||
{areas.map((area) => (
|
||||
<li className={selectedMapAreaId === area.id ? 'entity-card entity-card-active' : 'entity-card'} key={area.id}>
|
||||
<strong>{area.name}</strong>
|
||||
<div className="entity-meta">
|
||||
<span>{area.area_m2 ? `${area.area_m2.toFixed(2)} m2` : 'n/a'}</span>
|
||||
<span>{area.geometry?.type ?? area.geometry_type ?? 'n/a'}</span>
|
||||
<span>{area.area_m2 ? `${(area.area_m2 / 1_000_000).toLocaleString('nl-BE', { maximumFractionDigits: 2 })} km2` : 'Oppervlakte onbekend'}</span>
|
||||
<span>{area.geometry?.type ?? area.geometry_type ?? 'Geometrie onbekend'}</span>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-action"
|
||||
@@ -101,12 +101,12 @@ export function AreaPanel({
|
||||
disabled={!area.geometry}
|
||||
data-testid={`area-show-${area.id}`}
|
||||
>
|
||||
{selectedMapAreaId === area.id ? 'Shown on map' : 'Show on map'}
|
||||
{selectedMapAreaId === area.id ? 'Op kaart' : 'Toon op kaart'}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { FormEvent } from 'react'
|
||||
import type { ProjectCreate, ProjectRead } from '../../types'
|
||||
|
||||
const TECHNICAL_PROJECT_PATTERN = /^(GeoIntel Detection Quality Matrix|GeoIntel hard-negative|GeoIntel training|Mol Building QA)/i
|
||||
|
||||
function isTechnicalProject(project: ProjectRead): boolean {
|
||||
return TECHNICAL_PROJECT_PATTERN.test(project.name)
|
||||
}
|
||||
|
||||
interface ProjectPanelProps {
|
||||
projects: ProjectRead[]
|
||||
selectedProjectId: string | null
|
||||
@@ -27,68 +33,33 @@ export function ProjectPanel({
|
||||
onLoadDemoWorkflow,
|
||||
}: ProjectPanelProps): JSX.Element {
|
||||
const selectedProject = projects.find((project) => project.id === selectedProjectId)
|
||||
const regularProjects = projects.filter((project) => !isTechnicalProject(project))
|
||||
const technicalProjects = projects.filter(isTechnicalProject)
|
||||
const visibleProjects = selectedProject && isTechnicalProject(selectedProject)
|
||||
? [selectedProject, ...regularProjects]
|
||||
: regularProjects
|
||||
|
||||
return (
|
||||
<section className="workspace-panel" data-testid="project-panel">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<p className="eyebrow">Context</p>
|
||||
<h2>Projects</h2>
|
||||
<p className="eyebrow">Gegevenscontext</p>
|
||||
<h2>Werkruimtes</h2>
|
||||
</div>
|
||||
<span className="count-pill">{projects.length}</span>
|
||||
<span className="count-pill">{regularProjects.length} beschikbaar</span>
|
||||
</div>
|
||||
{loadingProjects ? <p>Loading projects...</p> : null}
|
||||
{loadingProjects ? <p>Werkruimtes laden...</p> : null}
|
||||
|
||||
<div className="data-selection-summary data-selection-summary-project">
|
||||
<span>Selected project</span>
|
||||
<strong>{selectedProject?.name ?? 'No active project'}</strong>
|
||||
<small>{selectedProject?.region ?? 'Create or load a project to start the workbench.'}</small>
|
||||
</div>
|
||||
|
||||
<details className="data-panel-form-block">
|
||||
<summary>Create project</summary>
|
||||
<form className="compact-form" onSubmit={onCreateProject}>
|
||||
<label>
|
||||
Name
|
||||
<input
|
||||
value={projectForm.name}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, name: event.target.value })}
|
||||
placeholder="Project name"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Description
|
||||
<input
|
||||
value={projectForm.description ?? ''}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, description: event.target.value })}
|
||||
placeholder="Description"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Region
|
||||
<input
|
||||
value={projectForm.region ?? 'Kempen'}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
|
||||
placeholder="Region"
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-action" type="submit">
|
||||
Create project
|
||||
</button>
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<div className="demo-actions">
|
||||
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
|
||||
{loadingDemoWorkflow ? 'Loading demo...' : 'Load demo workflow'}
|
||||
</button>
|
||||
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
|
||||
<span>Actieve werkruimte</span>
|
||||
<strong>{selectedProject?.name ?? 'Geen werkruimte actief'}</strong>
|
||||
<small>{selectedProject?.region ?? 'De regionale werkruimte wordt automatisch gekozen.'}</small>
|
||||
</div>
|
||||
|
||||
<div className="data-panel-list-block">
|
||||
<p className="data-section-label">Saved projects</p>
|
||||
<p className="data-section-label">Beschikbare werkruimtes</p>
|
||||
<ul className="entity-list">
|
||||
{projects.map((project) => (
|
||||
{visibleProjects.map((project) => (
|
||||
<li key={project.id}>
|
||||
<button
|
||||
className={project.id === selectedProjectId ? 'entity-card entity-card-active' : 'entity-card'}
|
||||
@@ -98,18 +69,78 @@ export function ProjectPanel({
|
||||
data-testid={`project-select-${project.id}`}
|
||||
>
|
||||
<strong>{project.name}</strong>
|
||||
<span>{project.region || 'No region'}</span>
|
||||
<span>{project.region || 'Regio niet opgegeven'}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
{projects.length === 0 ? (
|
||||
{regularProjects.length === 0 ? (
|
||||
<li className="empty-state">
|
||||
<strong>No projects yet</strong>
|
||||
<p>Create a project or load the offline demo workflow to populate the workbench.</p>
|
||||
<strong>Nog geen werkruimte beschikbaar</strong>
|
||||
<p>Maak een werkruimte aan of laad de demonstratiegegevens.</p>
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<details className="data-panel-form-block technical-management-block">
|
||||
<summary>Geavanceerd projectbeheer</summary>
|
||||
<div className="technical-management-body">
|
||||
<form className="compact-form" onSubmit={onCreateProject}>
|
||||
<label>
|
||||
Naam
|
||||
<input
|
||||
value={projectForm.name}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, name: event.target.value })}
|
||||
placeholder="Naam van de werkruimte"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Beschrijving
|
||||
<input
|
||||
value={projectForm.description ?? ''}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, description: event.target.value })}
|
||||
placeholder="Korte beschrijving"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Regio
|
||||
<input
|
||||
value={projectForm.region ?? 'Kempen'}
|
||||
onChange={(event) => onUpdateProjectForm({ ...projectForm, region: event.target.value })}
|
||||
placeholder="Regio"
|
||||
/>
|
||||
</label>
|
||||
<button className="primary-action" type="submit">Nieuwe werkruimte</button>
|
||||
</form>
|
||||
<div className="demo-actions">
|
||||
<button className="secondary-action" type="button" onClick={onLoadDemoWorkflow} disabled={loadingDemoWorkflow} data-testid="load-demo-workflow">
|
||||
{loadingDemoWorkflow ? 'Demo laden...' : 'Demonstratiegegevens laden'}
|
||||
</button>
|
||||
{demoWorkflowMessage ? <p>{demoWorkflowMessage}</p> : null}
|
||||
</div>
|
||||
{technicalProjects.length > 0 ? (
|
||||
<details className="technical-run-list">
|
||||
<summary>{technicalProjects.length} technische test- en benchmarkruns</summary>
|
||||
<ul className="entity-list">
|
||||
{technicalProjects.map((project) => (
|
||||
<li key={project.id}>
|
||||
<button
|
||||
className={project.id === selectedProjectId ? 'entity-card entity-card-active' : 'entity-card'}
|
||||
type="button"
|
||||
onClick={() => onSelectProject(project.id)}
|
||||
aria-pressed={project.id === selectedProjectId}
|
||||
data-testid={`project-select-${project.id}`}
|
||||
>
|
||||
<strong>{project.name}</strong>
|
||||
<span>{project.region || 'Technische run'}</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
</details>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,23 @@ interface ProviderPanelProps {
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
function providerStatusLabel(provider: ProviderCapability): string {
|
||||
if (provider.configured) {
|
||||
return 'Beschikbaar'
|
||||
}
|
||||
return 'Live koppeling uit'
|
||||
}
|
||||
|
||||
function authorityLabel(value: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
authoritative: 'Officieel',
|
||||
contextual: 'Aanvullend',
|
||||
manual: 'Eigen upload',
|
||||
fixture: 'Testgegevens',
|
||||
}
|
||||
return labels[value] ?? value
|
||||
}
|
||||
|
||||
export function ProviderPanel({
|
||||
providers,
|
||||
loadingCapabilities,
|
||||
@@ -18,40 +35,40 @@ export function ProviderPanel({
|
||||
<div className="system-provider-shell">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h2>Provider Capabilities</h2>
|
||||
<p className="muted">Review configured local providers and honest not-configured external sources.</p>
|
||||
<h2>Databronnen</h2>
|
||||
<p className="muted">Beschikbaarheid en herkomst van officiële, aanvullende en eigen bronnen.</p>
|
||||
</div>
|
||||
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
|
||||
Refresh providers
|
||||
Status vernieuwen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="system-provider-state-stack">
|
||||
{loadingCapabilities ? (
|
||||
<div className="result-state result-state-loading">
|
||||
<strong>Loading provider capabilities.</strong>
|
||||
<p>Checking local, fixture and external provider registry states.</p>
|
||||
<strong>Databronnen laden.</strong>
|
||||
<p>De lokale en externe bronstatus wordt gecontroleerd.</p>
|
||||
</div>
|
||||
) : null}
|
||||
{capabilitiesError ? (
|
||||
<div className="result-state result-state-error">
|
||||
<strong>Provider registry unavailable.</strong>
|
||||
<strong>Bronregister niet bereikbaar.</strong>
|
||||
<p>{capabilitiesError}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{providers.length === 0 && !loadingCapabilities ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No providers reported by backend.</strong>
|
||||
<p>Refresh provider capabilities after the backend is reachable.</p>
|
||||
<strong>Geen databronnen gemeld.</strong>
|
||||
<p>Vernieuw de status zodra de backend bereikbaar is.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
|
||||
<div className="provider-detail-stack">
|
||||
<strong>Official reference sources</strong>
|
||||
<strong>Officiële referentiebronnen</strong>
|
||||
<div>
|
||||
GRB and OSM are reference-data provider capabilities, not AI model choices. Manual upload is the configured path for real reference datasets today; fixtures remain test/demo only.
|
||||
Reeds ingeladen GRB-lagen blijven lokaal beschikbaar. De live GRB- en OSM-koppelingen staan uit en halen dus niet ongemerkt externe gegevens op.
|
||||
</div>
|
||||
</div>
|
||||
<ul className="system-provider-list">
|
||||
@@ -62,24 +79,16 @@ export function ProviderPanel({
|
||||
<strong>{provider.display_name}</strong>
|
||||
<span>{provider.provider_name}</span>
|
||||
</div>
|
||||
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{provider.status}</span>
|
||||
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{providerStatusLabel(provider)}</span>
|
||||
</div>
|
||||
<div className="system-provider-grid">
|
||||
<div>
|
||||
<span>Authority</span>
|
||||
<strong>{provider.authority_level}</strong>
|
||||
<span>Herkomst</span>
|
||||
<strong>{authorityLabel(provider.authority_level)}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Configured</span>
|
||||
<strong>{provider.configured ? 'yes' : 'no'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Geometry</span>
|
||||
<strong>{provider.supported_geometry_types.join(', ')}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Query modes</span>
|
||||
<strong>{provider.supported_query_modes.join(', ')}</strong>
|
||||
<span>Status</span>
|
||||
<strong>{providerStatusLabel(provider)}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
|
||||
@@ -89,22 +98,29 @@ export function ProviderPanel({
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="provider-detail-stack">
|
||||
<div>limitation: {provider.limitation_message}</div>
|
||||
{!provider.configured && provider.not_configured_reason ? (
|
||||
<div>reason: {provider.not_configured_reason}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="provider-provenance-grid">
|
||||
<div className="provider-provenance-card">
|
||||
<span>Attribution</span>
|
||||
<strong>{provider.attribution}</strong>
|
||||
<details className="provider-technical-details">
|
||||
<summary>Herkomst en technische mogelijkheden</summary>
|
||||
<div className="provider-detail-stack">
|
||||
<div>{provider.limitation_message}</div>
|
||||
{!provider.configured && provider.not_configured_reason ? (
|
||||
<div>{provider.not_configured_reason}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="provider-provenance-card">
|
||||
<span>License</span>
|
||||
<strong>{provider.license_note}</strong>
|
||||
<div className="provider-provenance-grid">
|
||||
<div className="provider-provenance-card">
|
||||
<span>Naamsvermelding</span>
|
||||
<strong>{provider.attribution}</strong>
|
||||
</div>
|
||||
<div className="provider-provenance-card">
|
||||
<span>Licentie</span>
|
||||
<strong>{provider.license_note}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="entity-meta">
|
||||
<span>Geometrie: {provider.supported_geometry_types.join(', ')}</span>
|
||||
<span>Zoekwijzen: {provider.supported_query_modes.join(', ')}</span>
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -147,50 +147,63 @@ export function QualityResultsPanel({
|
||||
const hasActiveQualityFilters = qualityStatusFilter !== 'all' || qualityTypeFilter !== 'all' || qualitySearchQuery.trim().length > 0
|
||||
|
||||
return (
|
||||
<section className="quality-results-panel" data-testid="quality-results-panel">
|
||||
<section className={qualityChecks.length === 0 ? 'quality-results-panel quality-results-panel-empty' : 'quality-results-panel'} data-testid="quality-results-panel">
|
||||
<div className="quality-results-shell">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h2>QA/QC Results</h2>
|
||||
<p className="muted">Persisted quality checks from dataset, detection and segmentation comparisons.</p>
|
||||
<h2>Kwaliteitsresultaten</h2>
|
||||
<p className="muted">Bewaarde controles van kaartlagen en beeldanalyses.</p>
|
||||
</div>
|
||||
<span className="count-pill">{qualityChecks.length} checks</span>
|
||||
<span className="count-pill">{qualityChecks.length} controles</span>
|
||||
</div>
|
||||
|
||||
<div className="quality-summary-surface" aria-label="QA/QC result summary">
|
||||
<div className="quality-summary-grid">
|
||||
<div>
|
||||
<span>Completed</span>
|
||||
<span>Afgerond</span>
|
||||
<strong>{completedChecks}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest score</span>
|
||||
<span>Laatste score</span>
|
||||
<strong>{latestCheck?.score ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest status</span>
|
||||
<span>Laatste status</span>
|
||||
<strong>{latestCheck?.status ?? 'waiting'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{qualityChecks.length === 0 ? (
|
||||
<div className="result-state result-state-empty quality-user-empty-state">
|
||||
<strong>Nog geen kwaliteitscontrole uitgevoerd</strong>
|
||||
<p>Selecteer een gebied op de kaart en vergelijk een bewaard resultaat met een officiële referentielaag.</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{qualityChecks.length > 0 ? (
|
||||
<details className="quality-advanced-disclosure">
|
||||
<summary>
|
||||
<span>Details, kaartbewijs en historiek</span>
|
||||
<strong>{qualityChecks.length} bewaarde controles</strong>
|
||||
</summary>
|
||||
<div className="quality-evidence-surface" aria-label="QA/QC dataset evidence">
|
||||
<div className="quality-handoff-grid" aria-label="QA/QC dataset handoff context">
|
||||
<div>
|
||||
<span>Candidate layers</span>
|
||||
<span>Te controleren lagen</span>
|
||||
<strong>{candidateDatasets.length}</strong>
|
||||
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'No candidate layer selected yet'}</p>
|
||||
<p className="quality-dataset-name">{candidateDatasets[0]?.name ?? 'Nog geen te controleren laag gekozen'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Reference layers</span>
|
||||
<span>Referentielagen</span>
|
||||
<strong>{referenceDatasets.length}</strong>
|
||||
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'No reference layer available yet'}</p>
|
||||
<p className="quality-dataset-name">{referenceDatasets[0]?.name ?? 'Nog geen referentielaag beschikbaar'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Latest comparison</span>
|
||||
<span>Laatste vergelijking</span>
|
||||
<strong>{latestCheck?.status ?? 'waiting'}</strong>
|
||||
<p className="quality-dataset-name">
|
||||
Candidate {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n/a'} / Reference {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n/a'}
|
||||
Te controleren: {latestCandidateName ?? latestCheck?.candidate_dataset_id ?? 'n.v.t.'} / referentie: {latestReferenceName ?? latestCheck?.reference_dataset_id ?? 'n.v.t.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -199,8 +212,8 @@ export function QualityResultsPanel({
|
||||
<div className="quality-drilldown-surface" aria-label="QA/QC evidence drilldown">
|
||||
<div className="panel-title-row">
|
||||
<div>
|
||||
<h3>QA/QC evidence drilldown</h3>
|
||||
<p className="muted">Inspect the persisted check, metric evidence and dataset handoff context before exporting or sharing results.</p>
|
||||
<h3>Bewijs van de kwaliteitscontrole</h3>
|
||||
<p className="muted">Bekijk de bewaarde vergelijking, scores en kaartlagen voordat je resultaten deelt.</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -208,7 +221,7 @@ export function QualityResultsPanel({
|
||||
onClick={() => setSelectedQualityCheckId(latestCheck?.id ?? null)}
|
||||
disabled={!latestCheck}
|
||||
>
|
||||
Inspect latest check
|
||||
Laatste controle bekijken
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -216,12 +229,12 @@ export function QualityResultsPanel({
|
||||
onClick={() => latestCheck?.id && onOpenEvidenceMap?.(latestCheck.id)}
|
||||
disabled={!latestCheck || evidenceLoading || !onOpenEvidenceMap}
|
||||
>
|
||||
{evidenceLoading ? 'Loading map evidence...' : 'Show latest on map'}
|
||||
{evidenceLoading ? 'Kaartbewijs laden...' : 'Laatste op kaart tonen'}
|
||||
</button>
|
||||
</div>
|
||||
{evidenceError ? (
|
||||
<div className="result-state result-state-error">
|
||||
<strong>QA/QC evidence overlay could not be loaded.</strong>
|
||||
<strong>Het bewijs kon niet op de kaart worden geladen.</strong>
|
||||
<p>{evidenceError}</p>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -229,22 +242,22 @@ export function QualityResultsPanel({
|
||||
<>
|
||||
<div className="quality-drilldown-grid">
|
||||
<div>
|
||||
<span>Selected check</span>
|
||||
<span>Geselecteerde controle</span>
|
||||
<strong>{selectedQualityCheck.check_type}</strong>
|
||||
<p>{selectedQualityCheck.id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Candidate layer</span>
|
||||
<span>Te controleren laag</span>
|
||||
<strong>{selectedCandidateName}</strong>
|
||||
<p>{selectedQualityCheck.candidate_dataset_id ?? 'candidate not stored'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Reference layer</span>
|
||||
<span>Referentielaag</span>
|
||||
<strong>{selectedReferenceName}</strong>
|
||||
<p>{selectedQualityCheck.reference_dataset_id}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Analysis run</span>
|
||||
<span>Analyserun</span>
|
||||
<strong>{selectedQualityCheck.analysis_run_id ?? 'n/a'}</strong>
|
||||
<p>Job: {selectedQualityCheck.job_id ?? 'n/a'}</p>
|
||||
</div>
|
||||
@@ -254,39 +267,39 @@ export function QualityResultsPanel({
|
||||
<p>Score: {selectedQualityCheck.score ?? 'n/a'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>Completed</span>
|
||||
<span>Afgerond</span>
|
||||
<strong>{formatQualityTimestamp(selectedQualityCheck.completed_at)}</strong>
|
||||
<p>Created: {formatQualityTimestamp(selectedQualityCheck.created_at)}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-evidence-token-grid">
|
||||
<div>
|
||||
<span>False positive evidence</span>
|
||||
<span>Onterecht gevonden</span>
|
||||
<strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_positive_count'))}</strong>
|
||||
<p>Candidate geometries not matched to the reference layer.</p>
|
||||
<p>Gevonden vormen zonder overeenkomst in de referentielaag.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>False negative evidence</span>
|
||||
<span>Gemiste objecten</span>
|
||||
<strong>{qualityMetricValue(metricByKey(selectedQualityCheck, 'false_negative_count'))}</strong>
|
||||
<p>Reference geometries not matched by the candidate layer.</p>
|
||||
<p>Objecten uit de referentielaag die niet werden gevonden.</p>
|
||||
</div>
|
||||
<div className="quality-evidence-map-handoff">
|
||||
<span>Map evidence handoff</span>
|
||||
<span>Controle op de kaart</span>
|
||||
<strong>{selectedCandidateName} / {selectedReferenceName}</strong>
|
||||
<p>Use the candidate and reference datasets as map layers for spatial review.</p>
|
||||
<p>Toon beide lagen samen op de kaart voor een ruimtelijke controle.</p>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-action"
|
||||
onClick={() => onOpenEvidenceMap?.(selectedQualityCheck.id)}
|
||||
disabled={evidenceLoading || !onOpenEvidenceMap}
|
||||
>
|
||||
{evidenceLoading ? 'Loading overlay...' : 'Show evidence overlay'}
|
||||
{evidenceLoading ? 'Lagen laden...' : 'Bewijs op kaart tonen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-feature-evidence-grid" aria-label="Feature-level QA/QC evidence">
|
||||
<div>
|
||||
<span>Matched feature ids</span>
|
||||
<span>Overeenkomende object-ID's</span>
|
||||
{selectedMatchEvidence.length > 0 ? (
|
||||
<ul>
|
||||
{selectedMatchEvidence.slice(0, 6).map((item, index) => (
|
||||
@@ -294,11 +307,11 @@ export function QualityResultsPanel({
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No matched feature ids persisted for this check.</p>
|
||||
<p>Voor deze controle zijn geen overeenkomende object-ID's bewaard.</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span>False positive feature ids</span>
|
||||
<span>ID's van onterecht gevonden objecten</span>
|
||||
{selectedFalsePositiveEvidence.length > 0 ? (
|
||||
<ul>
|
||||
{selectedFalsePositiveEvidence.slice(0, 6).map((item, index) => (
|
||||
@@ -306,11 +319,11 @@ export function QualityResultsPanel({
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No false-positive feature ids persisted for this check.</p>
|
||||
<p>Er zijn geen ID's van onterecht gevonden objecten bewaard.</p>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span>False negative feature ids</span>
|
||||
<span>ID's van gemiste objecten</span>
|
||||
{selectedFalseNegativeEvidence.length > 0 ? (
|
||||
<ul>
|
||||
{selectedFalseNegativeEvidence.slice(0, 6).map((item, index) => (
|
||||
@@ -318,19 +331,19 @@ export function QualityResultsPanel({
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No false-negative feature ids persisted for this check.</p>
|
||||
<p>Er zijn geen ID's van gemiste objecten bewaard.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-provenance-grid">
|
||||
<div>
|
||||
<span>Parameters</span>
|
||||
<span>Instellingen</span>
|
||||
<pre className="quality-provenance-pre">
|
||||
{JSON.stringify(selectedQualityCheck.parameters_json ?? {}, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<span>Findings</span>
|
||||
<span>Bevindingen</span>
|
||||
<pre className="quality-provenance-pre">
|
||||
{JSON.stringify(selectedQualityCheck.findings_json ?? {}, null, 2)}
|
||||
</pre>
|
||||
@@ -339,8 +352,8 @@ export function QualityResultsPanel({
|
||||
</>
|
||||
) : (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No QA/QC check selected.</strong>
|
||||
<p>Run or load a persisted QA/QC check to inspect metric and map evidence.</p>
|
||||
<strong>Geen kwaliteitscontrole geselecteerd.</strong>
|
||||
<p>Kies een bewaarde controle om scores en kaartbewijs te bekijken.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -354,21 +367,21 @@ export function QualityResultsPanel({
|
||||
disabled={!selectedProjectId}
|
||||
data-testid="refresh-quality-results"
|
||||
>
|
||||
Refresh QA/QC results
|
||||
Kwaliteitsresultaten vernieuwen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="quality-result-state-stack">
|
||||
{qualityChecksError ? (
|
||||
<div className="result-state result-state-error">
|
||||
<strong>QA/QC results could not be loaded.</strong>
|
||||
<strong>De kwaliteitsresultaten konden niet worden geladen.</strong>
|
||||
<p>{qualityChecksError}</p>
|
||||
</div>
|
||||
) : null}
|
||||
{qualityChecks.length === 0 ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No persisted QA/QC results yet</strong>
|
||||
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
|
||||
<strong>Nog geen bewaarde kwaliteitsresultaten</strong>
|
||||
<p>Vergelijk een kaartlaag of beeldanalyse met een referentielaag om deze werkruimte te vullen.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -376,7 +389,7 @@ export function QualityResultsPanel({
|
||||
{qualityChecks.length > 0 ? (
|
||||
<div className="quality-history-controls" aria-label="QA/QC result filters">
|
||||
<label>
|
||||
Search QA/QC results
|
||||
Kwaliteitsresultaten zoeken
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Type, id or dataset"
|
||||
@@ -387,7 +400,7 @@ export function QualityResultsPanel({
|
||||
<label>
|
||||
Status
|
||||
<select value={qualityStatusFilter} onChange={(event) => setQualityStatusFilter(event.target.value)}>
|
||||
<option value="all">All statuses</option>
|
||||
<option value="all">Alle statussen</option>
|
||||
{qualityStatuses.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{status}
|
||||
@@ -396,9 +409,9 @@ export function QualityResultsPanel({
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Check type
|
||||
Type controle
|
||||
<select value={qualityTypeFilter} onChange={(event) => setQualityTypeFilter(event.target.value)}>
|
||||
<option value="all">All check types</option>
|
||||
<option value="all">Alle types</option>
|
||||
{qualityTypes.map((checkType) => (
|
||||
<option key={checkType} value={checkType}>
|
||||
{checkType}
|
||||
@@ -417,33 +430,33 @@ export function QualityResultsPanel({
|
||||
}}
|
||||
disabled={!hasActiveQualityFilters && !showAllQualityChecks}
|
||||
>
|
||||
Reset view
|
||||
Filters wissen
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{filteredQualityChecks.length > 8 ? (
|
||||
<div className="list-limit-banner">
|
||||
<span className="quality-list-count">
|
||||
Showing {visibleQualityChecks.length} of {filteredQualityChecks.length} matching QA/QC results.
|
||||
{hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} older checks hidden.` : ' All matching checks shown.'}
|
||||
{visibleQualityChecks.length} van {filteredQualityChecks.length} kwaliteitsresultaten getoond.
|
||||
{hiddenQualityCheckCount > 0 ? ` ${hiddenQualityCheckCount} oudere controles verborgen.` : ' Alle resultaten zijn zichtbaar.'}
|
||||
</span>
|
||||
<button type="button" className="secondary-action" onClick={() => setShowAllQualityChecks((value) => !value)}>
|
||||
{showAllQualityChecks ? 'Show latest 8' : 'Show all QA/QC results'}
|
||||
{showAllQualityChecks ? 'Alleen laatste 8 tonen' : 'Alle kwaliteitsresultaten tonen'}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? (
|
||||
<div className="result-state result-state-empty">
|
||||
<strong>No QA/QC results match the current filters.</strong>
|
||||
<p>Clear the search, status or check type filter to return to the latest quality results.</p>
|
||||
<strong>Geen kwaliteitsresultaten voldoen aan de filters.</strong>
|
||||
<p>Wis de zoek-, status- of typefilter om de laatste resultaten opnieuw te tonen.</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="quality-history-surface" aria-label="QA/QC result history">
|
||||
<div className="panel-title-row">
|
||||
<h3>Result history</h3>
|
||||
<span className="count-pill">{visibleQualityChecks.length} shown</span>
|
||||
<h3>Historiek</h3>
|
||||
<span className="count-pill">{visibleQualityChecks.length} getoond</span>
|
||||
</div>
|
||||
<ul className="quality-check-list">
|
||||
{visibleQualityChecks.map((check) => (
|
||||
@@ -466,7 +479,7 @@ export function QualityResultsPanel({
|
||||
</div>
|
||||
<div className="quality-check-actions">
|
||||
<button type="button" className="secondary-action" onClick={() => setSelectedQualityCheckId(check.id)}>
|
||||
Inspect check
|
||||
Controle bekijken
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -474,7 +487,7 @@ export function QualityResultsPanel({
|
||||
onClick={() => onOpenEvidenceMap?.(check.id)}
|
||||
disabled={evidenceLoading || !onOpenEvidenceMap}
|
||||
>
|
||||
Show evidence on map
|
||||
Bewijs op kaart tonen
|
||||
</button>
|
||||
</div>
|
||||
<div className="quality-score-row">
|
||||
@@ -483,16 +496,16 @@ export function QualityResultsPanel({
|
||||
<strong>{check.score ?? 'n/a'}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Metrics</span>
|
||||
<span>Meetwaarden</span>
|
||||
<strong>{check.metrics.length}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>Quality check</span>
|
||||
<span>Controle-ID</span>
|
||||
<strong>{check.id}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div className="quality-metric-section">
|
||||
<span>Metric evidence</span>
|
||||
<span>Gemeten kwaliteit</span>
|
||||
<div className="quality-metric-grid">
|
||||
{CORE_METRIC_ORDER.map((metricKey) => {
|
||||
const metric = check.metrics.find((item) => item.metric_key === metricKey)
|
||||
@@ -506,7 +519,7 @@ export function QualityResultsPanel({
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<span className="metric-list-label">Raw metrics</span>
|
||||
<span className="metric-list-label">Alle meetwaarden</span>
|
||||
<ul className="metric-list">
|
||||
{check.metrics.map((metric) => (
|
||||
<li key={metric.id}>
|
||||
@@ -519,6 +532,8 @@ export function QualityResultsPanel({
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { DatasetCreateResponse, ProjectRead } from '../types'
|
||||
|
||||
export const PRIMARY_FOCUS_LABEL = 'Mol'
|
||||
export const PRIMARY_FOCUS_REGION = 'Mol, Kempen'
|
||||
export const REGIONAL_WORKSPACE_PROJECT_NAME = 'Kempen Regional Workbench'
|
||||
export const REGIONAL_WORKSPACE_LABEL = 'Kempen (28 gemeenten)'
|
||||
export const PRIMARY_FOCUS_CENTER: [number, number] = [5.1167, 51.1919]
|
||||
export const PRIMARY_FOCUS_AREA_NAME = 'Mol primary AOI'
|
||||
export const PRIMARY_FOCUS_MUNICIPALITY_PROJECT_NAME = 'Mol Municipality Workbench'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { detectionApi } from '../services/api'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
@@ -72,10 +72,10 @@ export function useDetectionWorkflow({
|
||||
const [detectionModelError, setDetectionModelError] = useState<string | null>(null)
|
||||
const [modelAssetError, setModelAssetError] = useState<string | null>(null)
|
||||
const [selectedDetectionDatasetId, setSelectedDetectionDatasetId] = useState('')
|
||||
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-placeholder')
|
||||
const [selectedDetectionModelId, setSelectedDetectionModelId] = useState('yolo-configured')
|
||||
const [selectedModelAssetId, setSelectedModelAssetId] = useState('')
|
||||
const [detectionTileManifestPath, setDetectionTileManifestPath] = useState('')
|
||||
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.5)
|
||||
const [detectionConfidenceThreshold, setDetectionConfidenceThreshold] = useState(0.15)
|
||||
const [runningDetection, setRunningDetection] = useState(false)
|
||||
const [detectionRunResult, setDetectionRunResult] = useState<DetectionRunResponse | null>(null)
|
||||
const [detectionRunError, setDetectionRunError] = useState<string | null>(null)
|
||||
@@ -98,6 +98,12 @@ export function useDetectionWorkflow({
|
||||
const [detectionCalibrationRows, setDetectionCalibrationRows] = useState<DetectionCalibrationRunRow[]>([])
|
||||
const [detectionCalibrationError, setDetectionCalibrationError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedDetectionDatasetId && rasterDatasets.length > 0) {
|
||||
setSelectedDetectionDatasetId(rasterDatasets[0].id)
|
||||
}
|
||||
}, [rasterDatasets, selectedDetectionDatasetId])
|
||||
|
||||
const loadDetectionModels = async () => {
|
||||
setLoadingDetectionModels(true)
|
||||
setDetectionModelError(null)
|
||||
@@ -105,8 +111,12 @@ export function useDetectionWorkflow({
|
||||
try {
|
||||
const response = await detectionApi.listModels()
|
||||
setDetectionModels(response.models)
|
||||
if (!response.models.some((model) => model.model_id === selectedDetectionModelId) && response.models.length > 0) {
|
||||
setSelectedDetectionModelId(response.models[0].model_id)
|
||||
const selectedModelStillAvailable = response.models.some((model) => model.model_id === selectedDetectionModelId)
|
||||
if (!selectedModelStillAvailable && response.models.length > 0) {
|
||||
const configuredModel = response.models.find(
|
||||
(model) => model.configured && model.model_id !== 'manual-fixture-detector',
|
||||
)
|
||||
setSelectedDetectionModelId(configuredModel?.model_id ?? response.models[0].model_id)
|
||||
}
|
||||
} catch (error) {
|
||||
setDetectionModelError(formatError(error, 'Failed to load detection models'))
|
||||
@@ -114,8 +124,16 @@ export function useDetectionWorkflow({
|
||||
try {
|
||||
const assetResponse = await detectionApi.listModelAssets()
|
||||
setModelAssets(assetResponse.items)
|
||||
if (!assetResponse.items.some((asset) => asset.model_asset_id === selectedModelAssetId)) {
|
||||
setSelectedModelAssetId('')
|
||||
const activeAsset = assetResponse.items.find((asset) => asset.active) ?? null
|
||||
const selectedAssetStillAvailable = assetResponse.items.some((asset) => asset.model_asset_id === selectedModelAssetId)
|
||||
const nextAssetId = selectedAssetStillAvailable ? selectedModelAssetId : activeAsset?.model_asset_id ?? ''
|
||||
setSelectedModelAssetId(nextAssetId)
|
||||
try {
|
||||
const preflight = await detectionApi.getYoloPreflight({ model_asset_id: nextAssetId || null })
|
||||
setYoloPreflight(preflight)
|
||||
setYoloPreflightError(null)
|
||||
} catch (error) {
|
||||
setYoloPreflightError(formatError(error, 'Failed to load YOLO preflight status'))
|
||||
}
|
||||
} catch (error) {
|
||||
setModelAssets([])
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
PRIMARY_FOCUS_AREA_GEOJSON,
|
||||
PRIMARY_FOCUS_AREA_NAME,
|
||||
PRIMARY_FOCUS_REGION,
|
||||
REGIONAL_WORKSPACE_PROJECT_NAME,
|
||||
isPrimaryFocusMunicipalityBoundaryDataset,
|
||||
isPrimaryFocusMunicipalityProject,
|
||||
isPrimaryFocusProject,
|
||||
@@ -79,6 +80,17 @@ export function useProjectWorkspace() {
|
||||
if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) {
|
||||
return selectedProjectId
|
||||
}
|
||||
const regionalProject = items.find((project) => project.name === REGIONAL_WORKSPACE_PROJECT_NAME)
|
||||
if (regionalProject) {
|
||||
try {
|
||||
const data = await fetchProjectData(regionalProject.id)
|
||||
if (data.areas.length > 0 && data.datasets.some((dataset) => dataset.status === 'ready')) {
|
||||
return regionalProject.id
|
||||
}
|
||||
} catch {
|
||||
// Continue with the municipality and completeness fallbacks when regional data is unavailable.
|
||||
}
|
||||
}
|
||||
const municipalityProject = items.find(isPrimaryFocusMunicipalityProject)
|
||||
if (municipalityProject) {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { DatasetCreateResponse } from '../types'
|
||||
|
||||
const DATASET_LABEL_BY_LAYER: Record<string, string> = {
|
||||
buildings: 'Bebouwing',
|
||||
roads: 'Wegen',
|
||||
water: 'Water',
|
||||
parcels: 'Percelen',
|
||||
population: 'Bevolking',
|
||||
forest: 'Bos en groen',
|
||||
regional_boundary: 'Grens vervoerregio Kempen',
|
||||
municipality_boundaries: 'Gemeentegrenzen Kempen',
|
||||
}
|
||||
|
||||
export function getDatasetDisplayName(dataset: DatasetCreateResponse): string {
|
||||
const layer = (dataset.reference_layer_name ?? dataset.source_metadata?.layer_name ?? '')
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
const label = DATASET_LABEL_BY_LAYER[layer]
|
||||
if (!label) {
|
||||
return dataset.name
|
||||
}
|
||||
const source = (dataset.source_name ?? dataset.source).toUpperCase()
|
||||
return `${label} · ${source}`
|
||||
}
|
||||
@@ -1493,6 +1493,285 @@ details.ai-lab-model-surface > summary strong {
|
||||
}
|
||||
}
|
||||
|
||||
/* End-user workbench simplification */
|
||||
|
||||
.geo-loaded-scope {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 0.65rem 0.7rem;
|
||||
background: #f7faf9;
|
||||
}
|
||||
|
||||
.geo-loaded-scope span,
|
||||
.geo-loaded-scope small {
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.geo-loaded-scope strong {
|
||||
color: var(--text);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.technical-management-body {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 0.8rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.technical-management-body > .compact-form {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.technical-run-list,
|
||||
.area-catalog-disclosure,
|
||||
.dataset-technical-details,
|
||||
.quality-advanced-disclosure,
|
||||
.provider-technical-details,
|
||||
.secondary-analysis-disclosure,
|
||||
.status-details-disclosure,
|
||||
.export-history-disclosure {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
background: #f8fafb;
|
||||
}
|
||||
|
||||
.technical-run-list > summary,
|
||||
.area-catalog-disclosure > summary,
|
||||
.dataset-technical-details > summary,
|
||||
.quality-advanced-disclosure > summary,
|
||||
.provider-technical-details > summary,
|
||||
.secondary-analysis-disclosure > summary,
|
||||
.status-details-disclosure > summary,
|
||||
.export-history-disclosure > summary {
|
||||
display: flex;
|
||||
min-height: 2.7rem;
|
||||
cursor: pointer;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.62rem 0.75rem;
|
||||
color: #33424d;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 750;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.technical-run-list > summary::-webkit-details-marker,
|
||||
.area-catalog-disclosure > summary::-webkit-details-marker,
|
||||
.dataset-technical-details > summary::-webkit-details-marker,
|
||||
.quality-advanced-disclosure > summary::-webkit-details-marker,
|
||||
.provider-technical-details > summary::-webkit-details-marker,
|
||||
.secondary-analysis-disclosure > summary::-webkit-details-marker,
|
||||
.status-details-disclosure > summary::-webkit-details-marker,
|
||||
.export-history-disclosure > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.technical-run-list > summary::after,
|
||||
.area-catalog-disclosure > summary::after,
|
||||
.dataset-technical-details > summary::after,
|
||||
.quality-advanced-disclosure > summary::after,
|
||||
.provider-technical-details > summary::after,
|
||||
.secondary-analysis-disclosure > summary::after,
|
||||
.status-details-disclosure > summary::after,
|
||||
.export-history-disclosure > summary::after {
|
||||
content: '+';
|
||||
margin-left: auto;
|
||||
color: var(--accent-strong);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.technical-run-list[open] > summary::after,
|
||||
.area-catalog-disclosure[open] > summary::after,
|
||||
.dataset-technical-details[open] > summary::after,
|
||||
.quality-advanced-disclosure[open] > summary::after,
|
||||
.provider-technical-details[open] > summary::after,
|
||||
.secondary-analysis-disclosure[open] > summary::after,
|
||||
.status-details-disclosure[open] > summary::after,
|
||||
.export-history-disclosure[open] > summary::after {
|
||||
content: '-';
|
||||
}
|
||||
|
||||
.technical-run-list .entity-list,
|
||||
.area-catalog-disclosure .entity-list {
|
||||
max-height: 28rem;
|
||||
overflow: auto;
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 0.65rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.quality-advanced-disclosure {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.quality-advanced-disclosure > summary strong {
|
||||
margin-right: 0.25rem;
|
||||
color: var(--ink);
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.quality-advanced-disclosure > div {
|
||||
margin: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.dataset-technical-details {
|
||||
margin-top: 0.65rem;
|
||||
}
|
||||
|
||||
.dataset-technical-details > div,
|
||||
.dataset-technical-details > button,
|
||||
.provider-technical-details > div {
|
||||
margin: 0 0.7rem 0.7rem;
|
||||
}
|
||||
|
||||
.dataset-technical-details > summary,
|
||||
.provider-technical-details > summary {
|
||||
min-height: 2.3rem;
|
||||
padding-block: 0.45rem;
|
||||
}
|
||||
|
||||
.dataset-card-primary-meta {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.ai-user-summary {
|
||||
order: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.ai-user-summary-card {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 0.25rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 6px;
|
||||
padding: 0.75rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.ai-user-summary-card-primary {
|
||||
border-color: #bfd8d2;
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
|
||||
.ai-user-summary-card-ready {
|
||||
border-left: 3px solid var(--success);
|
||||
}
|
||||
|
||||
.ai-user-summary-card span,
|
||||
.ai-user-summary-card p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.ai-user-summary-card strong {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.secondary-analysis-disclosure {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.secondary-analysis-disclosure > summary strong,
|
||||
.status-details-disclosure > summary strong,
|
||||
.export-history-disclosure > summary strong {
|
||||
margin-left: auto;
|
||||
color: var(--muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.secondary-analysis-disclosure > section,
|
||||
.secondary-analysis-disclosure > .workspace-panel {
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.status-details-disclosure > section {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
border-top: 1px solid var(--line);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.quality-results-panel-empty .quality-evidence-surface,
|
||||
.quality-results-panel-empty .quality-drilldown-surface,
|
||||
.quality-results-panel-empty .quality-control-surface,
|
||||
.quality-results-panel-empty .quality-history-surface {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quality-user-empty-state {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.export-summary-surface,
|
||||
.export-handoff-surface,
|
||||
.handoff-action-card-technical {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.latest-download-empty {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.export-history-body {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
border-top: 1px solid var(--line);
|
||||
padding: 0.75rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.export-history-body > div:first-child {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.workspace-grid-analysis {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.workspace-grid-analysis > section:nth-child(n),
|
||||
.workspace-grid-analysis > details:nth-child(n) {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.ai-user-summary {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.ai-user-summary {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.context-bar > div:nth-child(n + 3) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 621px) {
|
||||
.status-strip-grid {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
|
||||
Reference in New Issue
Block a user