Polish export system density
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-20 23:12:14 +02:00
parent 5544c92006
commit f50b25554b
8 changed files with 558 additions and 278 deletions
+2
View File
@@ -16,6 +16,8 @@ AI Labs uses the same mobile-density baseline for Detection and Segmentation mod
Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench.
Export Center now separates export summary, handoff readiness, artifact actions, current export states and export history into focused surfaces. Provider Capabilities uses the same system-workspace rhythm with loading/error/empty cards, provider capability cards and dedicated attribution/license provenance cards.
The right-side inspector also uses mobile-safe metadata and tool panels. Dataset filenames, checksums, bounds, job JSON, raster/vector operation inputs and inspector action groups wrap or scroll within the inspector instead of widening the workbench.
Export Preview shows a compact JSON summary before the payload and keeps large JSON/GeoJSON handoff artifacts inside a scroll-contained preview shell. HTML report artifacts remain download-only.
+251 -224
View File
@@ -105,238 +105,265 @@ export function ExportCenter({
return (
<section className="export-center" data-testid="export-center">
<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>
</div>
<span className="count-pill">{exports.length} exports</span>
</div>
<div className="quality-summary-grid">
<div>
<span>GeoJSON</span>
<strong>{geojsonExports}</strong>
</div>
<div>
<span>Reports</span>
<strong>{reportExports}</strong>
</div>
<div>
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
<div className="handoff-summary-card">
<div className="export-center-shell">
<div className="panel-title-row">
<div>
<h3>Handoff readiness</h3>
<p className="muted">Pick the artifact that matches the current review state before sharing data outside the workbench.</p>
<h2>Export Center</h2>
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
</div>
<span className="status-badge">{handoffStatus}</span>
<span className="count-pill">{exports.length} exports</span>
</div>
<div className="handoff-readiness-grid">
<div>
<span>Vector dataset</span>
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong>
</div>
<div>
<span>Detection run</span>
<strong>{selectedDetectionRunId || 'none'}</strong>
</div>
<div>
<span>Segmentation run</span>
<strong>{selectedSegmentationRunId || 'none'}</strong>
</div>
<div>
<span>Latest handoff artifact</span>
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'none'}</strong>
</div>
</div>
</div>
<div className="export-action-grid">
<div className="handoff-action-grid">
<div className="handoff-action-card">
<span>Refresh</span>
<strong>Export registry</strong>
<p>Reload the persisted artifact list for the selected project.</p>
<button
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId || loadingExports}
data-testid="refresh-exports"
>
Refresh exports
</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>
<button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
Export project report HTML
</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>
<button
type="button"
className="secondary-action"
onClick={onExportProjectMetadata}
disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata"
>
Export project metadata JSON
</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>
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
Export selected vector GeoJSON
</button>
</div>
<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">
<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>
</div>
{loadingExports ? (
<div className="result-state result-state-loading">
<strong>Loading export registry.</strong>
<p>Retrieving persisted artifacts for the selected project.</p>
</div>
) : null}
{exportError ? (
<div className="result-state result-state-error">
<strong>Export action failed.</strong>
<p>{exportError}</p>
</div>
) : null}
{latestExport ? (
<div className="latest-export-card">
<span>Latest export</span>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
{exports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports registered yet.</strong>
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
</div>
) : null}
{exports.length > 0 ? (
<div className="export-history-controls" aria-label="Export history filters">
<label>
Search exports
<input
type="search"
placeholder="Type, id or path"
value={exportSearchQuery}
onChange={(event) => setExportSearchQuery(event.target.value)}
/>
</label>
<label>
Type
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
<option value="all">All types</option>
{exportTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>
<label>
Status
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{exportStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setExportSearchQuery('')
setExportTypeFilter('all')
setExportStatusFilter('all')
setShowAllExports(false)
}}
disabled={!hasActiveExportFilters && !showAllExports}
>
Reset view
</button>
</div>
) : null}
{filteredExports.length > 10 ? (
<div className="list-limit-banner">
<span>
Showing {visibleExports.length} of {filteredExports.length} matching exports.
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
{showAllExports ? 'Show latest 10' : 'Show all exports'}
</button>
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports match the current filters.</strong>
<p>Clear the search, type or status filter to return to the latest artifacts.</p>
</div>
) : null}
<ul className="export-list">
{visibleExports.map((item) => (
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<span className="export-type-badge">{formatExportType(item.export_type)}</span>
<div className="entity-meta">
<span>export id: {item.id}</span>
{item.analysis_run_id ? <span>analysis run: {item.analysis_run_id}</span> : null}
{item.created_at ? <span>created: {item.created_at}</span> : null}
</div>
</div>
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
<div className="export-summary-surface" aria-label="Export summary">
<div className="quality-summary-grid">
<div>
<span>GeoJSON</span>
<strong>{geojsonExports}</strong>
</div>
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
<div className="button-row">
{canPreviewJson(item) ? (
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
Preview JSON content
<div>
<span>Reports</span>
<strong>{reportExports}</strong>
</div>
<div>
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
</div>
<div className="export-handoff-surface" aria-label="Export handoff readiness">
<div className="handoff-summary-card">
<div className="panel-title-row">
<div>
<h3>Handoff readiness</h3>
<p className="muted">Pick the artifact that matches the current review state before sharing data outside the workbench.</p>
</div>
<span className="status-badge">{handoffStatus}</span>
</div>
<div className="handoff-readiness-grid">
<div>
<span>Vector dataset</span>
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong>
</div>
<div>
<span>Detection run</span>
<strong>{selectedDetectionRunId || 'none'}</strong>
</div>
<div>
<span>Segmentation run</span>
<strong>{selectedSegmentationRunId || 'none'}</strong>
</div>
<div>
<span>Latest handoff artifact</span>
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'none'}</strong>
</div>
</div>
</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>
</div>
</div>
<div className="export-action-grid">
<div className="handoff-action-grid">
<div className="handoff-action-card">
<span>Refresh</span>
<strong>Export registry</strong>
<p>Reload the persisted artifact list for the selected project.</p>
<button
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId || loadingExports}
data-testid="refresh-exports"
>
Refresh exports
</button>
) : (
<span className="status-badge">download-only HTML artifact</span>
)}
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
Download artifact
</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>
<button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
Export project report HTML
</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>
<button
type="button"
className="secondary-action"
onClick={onExportProjectMetadata}
disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata"
>
Export project metadata JSON
</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>
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
Export selected vector GeoJSON
</button>
</div>
<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">
<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>
</div>
</div>
<div className="export-state-stack">
{loadingExports ? (
<div className="result-state result-state-loading">
<strong>Loading export registry.</strong>
<p>Retrieving persisted artifacts for the selected project.</p>
</div>
) : null}
{exportError ? (
<div className="result-state result-state-error">
<strong>Export action failed.</strong>
<p>{exportError}</p>
</div>
) : null}
{latestExport ? (
<div className="latest-export-card">
<span>Latest export</span>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
{exports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports registered yet.</strong>
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
</div>
) : null}
</div>
<div className="export-history-surface" aria-label="Export history">
<div>
<h3>Export history</h3>
<p className="muted">Review persisted artifacts and open JSON previews where supported.</p>
</div>
{exports.length > 0 ? (
<div className="export-history-controls" aria-label="Export history filters">
<label>
Search exports
<input
type="search"
placeholder="Type, id or path"
value={exportSearchQuery}
onChange={(event) => setExportSearchQuery(event.target.value)}
/>
</label>
<label>
Type
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
<option value="all">All types</option>
{exportTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>
<label>
Status
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{exportStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setExportSearchQuery('')
setExportTypeFilter('all')
setExportStatusFilter('all')
setShowAllExports(false)
}}
disabled={!hasActiveExportFilters && !showAllExports}
>
Reset view
</button>
</div>
</li>
))}
</ul>
) : null}
{filteredExports.length > 10 ? (
<div className="list-limit-banner">
<span>
Showing {visibleExports.length} of {filteredExports.length} matching exports.
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
{showAllExports ? 'Show latest 10' : 'Show all exports'}
</button>
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports match the current filters.</strong>
<p>Clear the search, type or status filter to return to the latest artifacts.</p>
</div>
) : null}
<ul className="export-list">
{visibleExports.map((item) => (
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<span className="export-type-badge">{formatExportType(item.export_type)}</span>
<div className="entity-meta">
<span>export id: {item.id}</span>
{item.analysis_run_id ? <span>analysis run: {item.analysis_run_id}</span> : null}
{item.created_at ? <span>created: {item.created_at}</span> : null}
</div>
</div>
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
</div>
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
<div className="button-row">
{canPreviewJson(item) ? (
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
Preview JSON content
</button>
) : (
<span className="status-badge">download-only HTML artifact</span>
)}
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
Download artifact
</button>
</div>
</li>
))}
</ul>
</div>
</div>
</section>
)
}
@@ -15,62 +15,95 @@ export function ProviderPanel({
}: ProviderPanelProps): JSX.Element {
return (
<section className="system-provider-panel">
<div className="panel-title-row">
<div>
<h2>Provider Capabilities</h2>
<p className="muted">Review configured local providers and honest not-configured external sources.</p>
<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>
</div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers
</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>
</div>
) : null}
{capabilitiesError ? (
<div className="result-state result-state-error">
<strong>Provider registry unavailable.</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>
</div>
) : null}
</div>
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
<div className="system-provider-header">
<div>
<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>
</div>
<div className="system-provider-grid">
<div>
<span>Authority</span>
<strong>{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>
</div>
</div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
{provider.supported_layers.map((layer) => (
<span className="provider-layer-chip" key={layer}>
{layer}
</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>
</div>
<div className="provider-provenance-card">
<span>License</span>
<strong>{provider.license_note}</strong>
</div>
</div>
</li>
))}
</ul>
</div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers
</button>
</div>
{loadingCapabilities ? <p>Loading provider capabilities...</p> : null}
{capabilitiesError ? <p className="error">{capabilitiesError}</p> : null}
{providers.length === 0 && !loadingCapabilities ? <p>No providers reported by backend</p> : null}
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
<div className="system-provider-header">
<div>
<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>
</div>
<div className="system-provider-grid">
<div>
<span>Authority</span>
<strong>{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>
</div>
</div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
{provider.supported_layers.map((layer) => (
<span className="provider-layer-chip" key={layer}>
{layer}
</span>
))}
</div>
<div>limitation: {provider.limitation_message}</div>
<div>attribution: {provider.attribution}</div>
<div>license: {provider.license_note}</div>
{!provider.configured && provider.not_configured_reason ? (
<div>reason: {provider.not_configured_reason}</div>
) : null}
</li>
))}
</ul>
</section>
)
}
+91
View File
@@ -1766,6 +1766,60 @@ button.entity-card {
margin: 0.8rem 0;
}
.export-center-shell,
.system-provider-shell {
display: grid;
gap: 0.85rem;
min-width: 0;
}
.export-summary-surface,
.export-handoff-surface,
.export-actions-surface,
.export-history-surface,
.system-provider-capability-surface {
display: grid;
gap: 0.68rem;
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.78rem;
background: linear-gradient(180deg, #ffffff, #f7fbf8);
box-shadow: 0 1px 0 rgba(19, 32, 24, 0.03);
}
.export-history-surface,
.system-provider-capability-surface {
background: #ffffff;
}
.export-center-shell .quality-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.58rem;
margin: 0;
}
.export-handoff-surface .handoff-summary-card {
margin-top: 0;
border: 0;
padding: 0;
background: transparent;
box-shadow: none;
}
.export-actions-surface .export-action-grid {
margin: 0;
}
.export-state-stack,
.system-provider-state-stack,
.provider-detail-stack {
display: grid;
gap: 0.5rem;
min-width: 0;
}
.export-action-grid button {
min-height: 2.8rem;
text-align: left;
@@ -2045,6 +2099,43 @@ button.entity-card {
font-weight: 800;
}
.provider-detail-stack {
color: var(--muted);
font-size: 0.84rem;
line-height: 1.36;
}
.provider-provenance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.5rem;
}
.provider-provenance-card {
min-width: 0;
border: 1px solid rgba(15, 118, 110, 0.16);
border-radius: 7px;
padding: 0.55rem;
background: #ffffff;
}
.provider-provenance-card span {
display: block;
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.provider-provenance-card strong {
display: block;
margin-top: 0.18rem;
overflow-wrap: anywhere;
font-size: 0.88rem;
line-height: 1.25;
}
.export-preview-panel .job-result {
max-height: 38rem;
}