Polish workbench visual shell
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-18 23:24:08 +02:00
parent c85953f389
commit 6fa41e11e4
11 changed files with 317 additions and 70 deletions
+16
View File
@@ -459,6 +459,22 @@ function App(): JSX.Element {
</div>
<p>{activeWorkspaceItem.description}</p>
</div>
<div className="workspace-command-bar" aria-label="Workspace shortcuts">
<div className="workspace-nav-cluster">
{workspaceNavItems.slice(0, 5).map((item) => (
<button
key={item.key}
type="button"
className={item.key === activeWorkspace ? 'command-chip command-chip-active' : 'command-chip'}
onClick={() => setActiveWorkspace(item.key)}
aria-pressed={item.key === activeWorkspace}
>
{item.label}
</button>
))}
</div>
<span>{selectedProject ? `Active project: ${selectedProject.name}` : 'Start with a project or load the demo workflow.'}</span>
</div>
{activeWorkspace === 'overview' ? (
<div className="workspace-stack">
@@ -70,7 +70,7 @@ export function DatasetPanel({
const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length
return (
<section data-testid="dataset-panel">
<section className="workspace-panel" data-testid="dataset-panel">
<div className="panel-title-row">
<div>
<p className="eyebrow">Data catalog</p>
@@ -127,7 +127,12 @@ export function DatasetPanel({
</form>
{loadingDatasets ? <p>Loading datasets...</p> : null}
{datasets.length === 0 ? <p>No datasets yet</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>
</div>
) : null}
<ul className="dataset-list">
{datasets.map((dataset) => (
<li className={dataset.id === selectedDatasetId ? 'dataset-card dataset-card-active' : 'dataset-card'} key={dataset.id}>
@@ -144,7 +149,7 @@ export function DatasetPanel({
{dataset.status === 'ready' ? 'ready' : dataset.status === 'failed' ? 'failed' : 'pending'}
</span>
</div>
<div className="dataset-metrics">
<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>
@@ -85,7 +85,7 @@ export function DetectionLab({
onRunQa,
}: DetectionLabProps): JSX.Element {
return (
<section>
<section className="workspace-panel">
<div className="panel-title-row">
<div>
<p className="eyebrow">Object detection</p>
@@ -166,7 +166,7 @@ export function DetectionLab({
</div>
{detectionRunError ? <p className="error">{detectionRunError}</p> : null}
{detectionRunResult ? (
<div>
<div className="result-summary-card">
<p>Status: {detectionRunResult.status}</p>
<p>Message: {detectionRunResult.message}</p>
<p>Analysis run: {detectionRunResult.analysis_run_id}</p>
@@ -221,26 +221,28 @@ export function DetectionLab({
{loadingDetectionResults ? <p>Loading detection results...</p> : null}
<p>Detections loaded: {detectionItems.length}</p>
{detectionItems.length > 0 ? (
<table>
<thead>
<tr>
<th>Class</th>
<th>Confidence</th>
<th>Model</th>
<th>Source tile</th>
</tr>
</thead>
<tbody>
{detectionItems.map((detection) => (
<tr key={detection.id}>
<td>{detection.class_name}</td>
<td>{detection.confidence.toFixed(2)}</td>
<td>{detection.model_name}</td>
<td>{detection.source_tile_path || 'n/a'}</td>
<div className="table-scroll">
<table>
<thead>
<tr>
<th>Class</th>
<th>Confidence</th>
<th>Model</th>
<th>Source tile</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{detectionItems.map((detection) => (
<tr key={detection.id}>
<td>{detection.class_name}</td>
<td>{detection.confidence.toFixed(2)}</td>
<td>{detection.model_name}</td>
<td>{detection.source_tile_path || 'n/a'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</div>
<div className="lab-block">
@@ -261,7 +263,7 @@ export function DetectionLab({
</button>
{detectionQaError ? <p className="error">{detectionQaError}</p> : null}
{detectionQaResult ? (
<div>
<div className="result-summary-card">
<p>Status: {detectionQaResult.status}</p>
<p>Quality check: {detectionQaResult.quality_check_id}</p>
<p>Precision: {detectionQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
@@ -27,7 +27,7 @@ export function ProjectPanel({
onLoadDemoWorkflow,
}: ProjectPanelProps): JSX.Element {
return (
<section data-testid="project-panel">
<section className="workspace-panel" data-testid="project-panel">
<div className="panel-title-row">
<div>
<p className="eyebrow">Context</p>
@@ -89,7 +89,12 @@ export function ProjectPanel({
</button>
</li>
))}
{projects.length === 0 ? <li>No projects yet</li> : null}
{projects.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>
</li>
) : null}
</ul>
</section>
)
@@ -85,7 +85,7 @@ export function SegmentationLab({
onRunQa,
}: SegmentationLabProps): JSX.Element {
return (
<section>
<section className="workspace-panel">
<div className="panel-title-row">
<div>
<p className="eyebrow">Polygon segmentation</p>
@@ -163,7 +163,7 @@ export function SegmentationLab({
) : null}
{segmentationRunError ? <p className="error">{segmentationRunError}</p> : null}
{segmentationRunResult ? (
<div>
<div className="result-summary-card">
<p>Status: {segmentationRunResult.status}</p>
<p>Message: {segmentationRunResult.message}</p>
<p>Analysis run: {segmentationRunResult.analysis_run_id}</p>
@@ -218,30 +218,32 @@ export function SegmentationLab({
{loadingSegmentationResults ? <p>Loading segmentation results...</p> : null}
<p>Segmentations loaded: {segmentationItems.length}</p>
{segmentationItems.length > 0 ? (
<table>
<thead>
<tr>
<th>Class</th>
<th>Confidence</th>
<th>Area m2</th>
<th>Model</th>
<th>Tile</th>
<th>Mask path</th>
</tr>
</thead>
<tbody>
{segmentationItems.map((segmentation) => (
<tr key={segmentation.id}>
<td>{segmentation.class_name}</td>
<td>{segmentation.confidence?.toFixed(2) ?? 'n/a'}</td>
<td>{segmentation.area_m2?.toFixed(2) ?? 'n/a'}</td>
<td>{segmentation.model_name}</td>
<td>{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}</td>
<td>{segmentation.mask_path || 'n/a'}</td>
<div className="table-scroll">
<table>
<thead>
<tr>
<th>Class</th>
<th>Confidence</th>
<th>Area m2</th>
<th>Model</th>
<th>Tile</th>
<th>Mask path</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{segmentationItems.map((segmentation) => (
<tr key={segmentation.id}>
<td>{segmentation.class_name}</td>
<td>{segmentation.confidence?.toFixed(2) ?? 'n/a'}</td>
<td>{segmentation.area_m2?.toFixed(2) ?? 'n/a'}</td>
<td>{segmentation.model_name}</td>
<td>{segmentation.source_tile_path || (segmentation.tile_index ?? 'n/a')}</td>
<td>{segmentation.mask_path || 'n/a'}</td>
</tr>
))}
</tbody>
</table>
</div>
) : null}
</div>
<div className="lab-block">
@@ -262,7 +264,7 @@ export function SegmentationLab({
</button>
{segmentationQaError ? <p className="error">{segmentationQaError}</p> : null}
{segmentationQaResult ? (
<div>
<div className="result-summary-card">
<p>Status: {segmentationQaResult.status}</p>
<p>Quality check: {segmentationQaResult.quality_check_id}</p>
<p>Precision: {segmentationQaResult.precision?.toFixed(3) ?? 'n/a'}</p>
+152 -17
View File
@@ -4,6 +4,8 @@
--bg: #eef4f1;
--panel: #ffffff;
--panel-soft: #f8fbf9;
--surface-raised: #ffffff;
--surface-sunken: #f3f8f5;
--text: #132018;
--muted: #5f6f67;
--line: #cbd8d0;
@@ -14,6 +16,7 @@
--warning: #b45309;
--danger: #991b1b;
--shadow: 0 12px 32px rgba(33, 48, 41, 0.08);
--shadow-soft: 0 8px 24px rgba(33, 48, 41, 0.06);
color-scheme: light;
}
@@ -32,6 +35,8 @@ body {
background:
linear-gradient(180deg, rgba(15, 118, 110, 0.08), rgba(238, 244, 241, 0) 18rem),
var(--bg);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
button,
@@ -183,8 +188,8 @@ section {
border: 1px solid var(--line);
border-radius: 8px;
padding: 1rem;
background: rgba(255, 255, 255, 0.94);
box-shadow: var(--shadow);
background: rgba(255, 255, 255, 0.96);
box-shadow: var(--shadow-soft);
}
.workspace-grid {
@@ -519,12 +524,12 @@ section li strong + div {
top: 0;
z-index: 20;
display: grid;
grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
gap: 0.75rem;
grid-template-columns: minmax(12rem, 17rem) minmax(0, 1fr);
gap: 0.9rem;
align-items: center;
border-bottom: 1px solid var(--line);
padding: 0.55rem 0.85rem;
background: rgba(248, 251, 249, 0.96);
padding: 0.58rem 1rem;
background: rgba(250, 253, 251, 0.97);
backdrop-filter: blur(12px);
}
@@ -545,11 +550,11 @@ section li strong + div {
}
.context-bar > div {
min-height: 3.2rem;
min-height: 3rem;
border: 1px solid var(--line);
border-radius: 8px;
border-radius: 7px;
padding: 0.44rem 0.58rem;
background: #ffffff;
background: var(--surface-raised);
}
.context-bar span {
@@ -581,7 +586,7 @@ section li strong + div {
min-height: 0;
overflow: auto;
border-color: var(--line);
background: #f8fbf9;
background: var(--surface-sunken);
}
.workbench-sidebar {
@@ -636,7 +641,7 @@ section li strong + div {
min-width: 0;
min-height: 0;
overflow: auto;
padding: 0.85rem 1rem 1rem;
padding: 0.9rem 1.05rem 1.15rem;
}
.workbench-inspector {
@@ -656,11 +661,12 @@ section li strong + div {
gap: 1rem;
align-items: end;
justify-content: space-between;
margin-bottom: 0.85rem;
margin-bottom: 0.65rem;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.72rem 0.85rem;
background: #ffffff;
border-radius: 7px;
padding: 0.76rem 0.9rem;
background: var(--surface-raised);
box-shadow: var(--shadow-soft);
}
.workspace-heading h2 {
@@ -674,6 +680,50 @@ section li strong + div {
text-align: right;
}
.workspace-command-bar {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.8rem;
align-items: center;
margin-bottom: 0.85rem;
border: 1px solid var(--line);
border-radius: 7px;
padding: 0.48rem;
background: rgba(255, 255, 255, 0.74);
}
.workspace-command-bar > span {
max-width: 24rem;
padding-right: 0.35rem;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.3;
text-align: right;
}
.workspace-nav-cluster {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.command-chip {
width: auto;
min-height: 2.05rem;
border-color: transparent;
padding: 0.32rem 0.62rem;
background: transparent;
box-shadow: none;
color: var(--muted);
font-size: 0.8rem;
}
.command-chip-active {
border-color: rgba(15, 118, 110, 0.34);
background: var(--accent-soft);
color: var(--accent-strong);
}
.workspace-stack {
display: grid;
gap: 1rem;
@@ -706,6 +756,11 @@ section li strong + div {
overflow: visible;
}
.workspace-panel {
border-radius: 7px;
background: var(--surface-raised);
}
.workspace-grid-data > section:nth-child(3) {
grid-column: 1 / -1;
}
@@ -768,6 +823,28 @@ section th {
text-transform: uppercase;
}
.table-scroll {
width: 100%;
max-height: 22rem;
overflow: auto;
margin-top: 0.75rem;
border: 1px solid var(--line);
border-radius: 7px;
background: #ffffff;
}
.table-scroll table {
min-width: 42rem;
margin-top: 0;
}
.table-scroll thead th {
position: sticky;
top: 0;
z-index: 1;
background: #f8fbf9;
}
.workbench-inspector .job-result,
.workbench-inspector pre {
max-height: 16rem;
@@ -854,6 +931,13 @@ section th {
background: var(--panel-soft);
}
.data-density-grid {
border: 1px solid var(--line);
border-radius: 7px;
padding: 0.55rem;
background: #ffffff;
}
.entity-list,
.dataset-list,
.model-list,
@@ -922,6 +1006,14 @@ button.entity-card {
padding: 0.85rem;
}
.dataset-card,
.model-card,
.quality-check-card,
.export-card,
.lab-block {
box-shadow: 0 1px 0 rgba(19, 32, 24, 0.03);
}
.dataset-card-active {
border-color: rgba(15, 118, 110, 0.5);
box-shadow: inset 4px 0 0 var(--accent);
@@ -974,6 +1066,24 @@ button.entity-card {
width: auto;
}
.result-summary-card {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: 0.45rem 0.7rem;
margin-top: 0.75rem;
border: 1px solid rgba(15, 118, 110, 0.22);
border-radius: 7px;
padding: 0.72rem;
background: var(--accent-soft);
}
.result-summary-card p {
margin: 0;
color: #23423b;
font-size: 0.86rem;
line-height: 1.35;
}
.quality-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -1056,6 +1166,7 @@ button.entity-card {
gap: 0.25rem;
margin-top: 0.75rem;
background: #ffffff;
color: var(--text);
}
.empty-state p,
@@ -1379,11 +1490,13 @@ button.entity-card {
}
.workspace-heading,
.workspace-command-bar,
.status-strip-header {
display: block;
}
.workspace-heading p:last-child,
.workspace-command-bar > span,
.status-next-action {
margin-top: 0.45rem;
text-align: left;
@@ -1419,11 +1532,33 @@ button.entity-card {
}
.context-bar,
.workbench-sidebar nav,
.quick-action-grid {
.quick-action-grid,
.workspace-nav-cluster {
grid-template-columns: 1fr;
}
.workbench-sidebar nav {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding-bottom: 0.2rem;
}
.workbench-sidebar .nav-item {
min-width: 8.75rem;
}
.workspace-nav-cluster {
display: flex;
flex-wrap: nowrap;
overflow-x: auto;
padding-bottom: 0.1rem;
}
.command-chip {
min-width: 6.75rem;
}
.status-strip-grid {
grid-template-columns: 1fr;
}