diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f4fdea4..e100ced9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 88 AI Labs density polish (2026-06-20) + +- Grouped Detection Lab and Segmentation Lab model registry, run controls, result loading and QA controls into focused surfaces. +- Added shared AI Lab density CSS for model lists, run forms, result/QA summaries and mobile-safe grids. +- Preserved existing detection/segmentation model loading, run, result filtering and QA callbacks without API or persistence changes. +- Added static regression coverage for AI Lab hierarchy and density contracts. +- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed. + ## Sprint 87 Change Detection density polish (2026-06-20) - Grouped Change Detection heading, input controls, result states, summary and warnings into focused surfaces. diff --git a/backend/tests/test_sprint88_ai_lab_density.py b/backend/tests/test_sprint88_ai_lab_density.py new file mode 100644 index 00000000..02359c11 --- /dev/null +++ b/backend/tests/test_sprint88_ai_lab_density.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_detection_lab_exposes_structured_surfaces() -> None: + lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( + encoding="utf-8" + ) + + assert 'className="workspace-panel ai-lab-shell detection-lab-shell"' in lab + assert 'className="ai-lab-model-surface"' in lab + assert 'aria-label="Detection model capabilities"' in lab + assert 'className="ai-lab-run-surface"' in lab + assert 'aria-label="Detection run controls"' in lab + assert 'className="ai-lab-state-stack"' in lab + assert 'className="ai-lab-results-surface"' in lab + assert 'aria-label="Detection results"' in lab + assert 'className="ai-lab-qa-surface"' in lab + assert 'aria-label="Detection QA controls and results"' in lab + + +def test_segmentation_lab_exposes_structured_surfaces() -> None: + lab = (ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx").read_text( + encoding="utf-8" + ) + + assert 'className="workspace-panel ai-lab-shell segmentation-lab-shell"' in lab + assert 'className="ai-lab-model-surface"' in lab + assert 'aria-label="Segmentation model capabilities"' in lab + assert 'className="ai-lab-run-surface"' in lab + assert 'aria-label="Segmentation run controls"' in lab + assert 'className="ai-lab-state-stack"' in lab + assert 'className="ai-lab-results-surface"' in lab + assert 'aria-label="Segmentation results"' in lab + assert 'className="ai-lab-qa-surface"' in lab + assert 'aria-label="Segmentation QA controls and results"' in lab + + +def test_ai_lab_preserves_existing_detection_and_segmentation_controls() -> None: + detection = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( + encoding="utf-8" + ) + segmentation = (ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx").read_text( + encoding="utf-8" + ) + + assert "onRunDetection" in detection + assert "onLoadResults" in detection + assert "onRunQa" in detection + assert "detectionTileManifestPath" in detection + assert "Detections loaded: {detectionItems.length}" in detection + assert "Compare detections to reference" in detection + + assert "onRunSegmentation" in segmentation + assert "onLoadResults" in segmentation + assert "onRunQa" in segmentation + assert "selectedSegmentationModelConfigured" in segmentation + assert "Segmentations loaded: {segmentationItems.length}" in segmentation + assert "Compare segmentations to reference" in segmentation + + +def test_ai_lab_density_css_contracts() -> None: + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert ".ai-lab-shell" in css + assert ".ai-lab-model-surface" in css + assert ".ai-lab-run-surface" in css + assert ".ai-lab-results-surface" in css + assert ".ai-lab-qa-surface" in css + assert ".ai-lab-state-stack" in css + assert ".ai-lab-shell .model-list" in css + assert ".ai-lab-shell .lab-form-grid" in css + assert ".ai-lab-summary-grid" in css + assert "grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));" in css + assert "grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));" in css diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index d46b151b..944d923b 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -3175,3 +3175,38 @@ Limitations: Next recommended pass: - Continue with AI Labs run-form hierarchy and detection/segmentation result density after this pass is deployed and visually checked. + +## Sprint 88 AI Labs density polish (2026-06-20) + +Changed: +- Wrapped Detection Lab and Segmentation Lab in shared `ai-lab-shell` workspace shells. +- Grouped model registry states into `ai-lab-model-surface` with a shared `ai-lab-state-stack`. +- Grouped run controls into `ai-lab-run-surface`. +- Grouped persisted detection/segmentation result loading and tables into `ai-lab-results-surface`. +- Grouped QA controls and metric summaries into `ai-lab-qa-surface`. +- Added shared AI Lab CSS contracts for compact model grids, form grids and summary grids. +- Added `backend/tests/test_sprint88_ai_lab_density.py`. +- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`. + +Tested: +- Red step: `python -m pytest backend/tests/test_sprint88_ai_lab_density.py -q` failed on missing AI Lab shells/surfaces and CSS contracts. +- `python -m pytest backend/tests/test_sprint88_ai_lab_density.py backend/tests/test_sprint8c_detection_visualization_qa.py backend/tests/test_sprint9_segmentation_foundation.py backend/tests/test_sprint39_frontend_orchestration_hooks.py backend/tests/test_sprint47_workbench_interaction_smoke.py -q` (`34 passed`) +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- Local browser UI check against `http://127.0.0.1:5174` passed on desktop and mobile: Detection/Segmentation shells and model/run/result/QA surfaces rendered, no horizontal overflow and no console warnings/errors. The local-only screenshot showed expected API 500 states because only the frontend Vite server was running. +- First full readiness run caught legacy AI Lab static contract expectations for exact `lab-block` and `result-summary-card` class names. Kept those compatibility anchors while preserving the new AI Lab surfaces. +- `python -m pytest backend/tests/test_sprint50_workspace_usability_polish.py backend/tests/test_sprint75_ai_labs_mobile_polish.py backend/tests/test_sprint88_ai_lab_density.py -q` (`8 passed`) +- `bash scripts/run_readiness_check.sh` (`299 passed`; frontend typecheck/build passed; Alembic head `202606120900`; live smoke syntax passed) +- Tower deploy via `scripts/deploy_tower.ps1` rebuilt and restarted the all-in-one container on `http://192.168.10.150:1202`. +- Tower deploy live migration smoke passed with PostGIS `3.6 USE_GEOS=1 USE_PROJ=1 USE_STATS=1` and required runtime schema objects present. +- Browser runtime verification passed for frontend, proxied API and icon. +- Live browser AI Labs check against `http://192.168.10.150:1202` passed on desktop and mobile: Detection/Segmentation shells and model/run/result/QA surfaces rendered, no horizontal overflow and no console warnings/errors. + +Open: +- None known after full readiness, Tower deploy and live browser validation. + +Limitations: +- Frontend AI Labs presentation hierarchy only; no detection/segmentation behavior, API contract, persistence, migration, provider fetching or AI/model changes. + +Next recommended pass: +- Continue with export/system final visual consistency or live workflow guidance once this pass is deployed and visually checked. diff --git a/docs/TODO.md b/docs/TODO.md index 7aa6fb1e..08ffe036 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -77,6 +77,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add Map workspace panel hierarchy and layer-control density polish. - [x] Add QA/QC workspace result hierarchy and filter density polish. - [x] Add Change Detection panel hierarchy and analysis workspace density polish. +- [x] Add AI Labs Detection/Segmentation hierarchy and result density polish. ## Sprint 8 status diff --git a/frontend/README.md b/frontend/README.md index 7ca4a2c2..f6ce38d7 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -26,6 +26,8 @@ Raster and vector operation panels use structured group headings, compact helper QA/QC, exports and AI lab result panels use shared loading, error, empty and ready state cards. This keeps model registry failures, empty histories and result counts visually consistent across the workbench. +Detection Lab and Segmentation Lab now share the same AI workspace hierarchy: model capabilities, run controls, persisted results and QA controls are separated into focused surfaces. Existing run, filter, result loading and QA callbacks remain unchanged, but the screens are denser and easier to scan on desktop and mobile. + ## Scope implemented - API client layer (`src/services/api`) - Project and area list/create flows diff --git a/frontend/src/components/detection/DetectionLab.tsx b/frontend/src/components/detection/DetectionLab.tsx index a418831a..e8155db4 100644 --- a/frontend/src/components/detection/DetectionLab.tsx +++ b/frontend/src/components/detection/DetectionLab.tsx @@ -85,7 +85,7 @@ export function DetectionLab({ onRunQa, }: DetectionLabProps): JSX.Element { return ( -
+

Object detection

@@ -95,109 +95,130 @@ export function DetectionLab({ Refresh models
- {loadingDetectionModels ? ( -
- Loading detection models. -

Checking backend model registry availability.

+ +
+
+
+

Model registry

+

Backend-reported detector states and limitations.

+
- ) : null} - {detectionModelError ? ( -
- Detection model registry unavailable. -

{detectionModelError}

-
- ) : null} - {detectionModels.length === 0 && !loadingDetectionModels ? ( -
- No detection models reported by backend. -

Refresh models after the backend is reachable.

-
- ) : null} -
    - {detectionModels.map((model) => ( -
  • - {model.display_name} - {model.status} -
    - {model.model_id} - {model.framework} - {model.task_type} +
    + {loadingDetectionModels ? ( +
    + Loading detection models. +

    Checking backend model registry availability.

    -

    classes: {model.supported_classes.join(', ')}

    -

    {model.limitation_message}

    -
  • - ))} -
-
-

Run detection

-
- - - + ) : null} + {detectionModelError ? ( +
+ Detection model registry unavailable. +

{detectionModelError}

+
+ ) : null} + {detectionModels.length === 0 && !loadingDetectionModels ? ( +
+ No detection models reported by backend. +

Refresh models after the backend is reachable.

+
+ ) : null}
- {selectedDetectionModelId === 'yolo-configured' ? ( - - ) : null} - +
    + {detectionModels.map((model) => ( +
  • + {model.display_name} + {model.status} +
    + {model.model_id} + {model.framework} + {model.task_type} +
    +

    classes: {model.supported_classes.join(', ')}

    +

    {model.limitation_message}

    +
  • + ))} +
- {detectionRunError ? ( -
- Detection run failed. -

{detectionRunError}

-
- ) : null} - {detectionRunResult ? ( -
-

Status: {detectionRunResult.status}

-

Message: {detectionRunResult.message}

-

Analysis run: {detectionRunResult.analysis_run_id}

-

Job: {detectionRunResult.job_id}

-

Detections: {detectionRunResult.detection_count}

- {detectionRunResult.error_code ?

Code: {detectionRunResult.error_code}

: null} -
- ) : null} +
+
+

Run detection

+
+ + + +
+ {selectedDetectionModelId === 'yolo-configured' ? ( + + ) : null} + +
+
+ +
+ {detectionRunError ? ( +
+ Detection run failed. +

{detectionRunError}

+
+ ) : null} + {detectionRunResult ? ( +
+

Status: {detectionRunResult.status}

+

Message: {detectionRunResult.message}

+

Analysis run: {detectionRunResult.analysis_run_id}

+

Job: {detectionRunResult.job_id}

+

Detections: {detectionRunResult.detection_count}

+ {detectionRunResult.error_code ?

Code: {detectionRunResult.error_code}

: null} +
+ ) : null} +
+ +
-

Detection results

+
+

Detection results

+

Load persisted detections and filter by class or confidence.

+
@@ -244,9 +265,11 @@ export function DetectionLab({

Retrieving persisted detections for the selected run.

) : null} -
- Detections loaded: {detectionItems.length} -

{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}

+
+
+ Detections loaded: {detectionItems.length} +

{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}

+
{detectionItems.length > 0 ? (
@@ -273,7 +296,8 @@ export function DetectionLab({
) : null}
-
+ +

Detection QA

+ ) : null} + {detectionQaResult ? (

Status: {detectionQaResult.status}

Quality check: {detectionQaResult.quality_check_id}

diff --git a/frontend/src/components/segmentation/SegmentationLab.tsx b/frontend/src/components/segmentation/SegmentationLab.tsx index e643bf52..264b87ba 100644 --- a/frontend/src/components/segmentation/SegmentationLab.tsx +++ b/frontend/src/components/segmentation/SegmentationLab.tsx @@ -85,7 +85,7 @@ export function SegmentationLab({ onRunQa, }: SegmentationLabProps): JSX.Element { return ( -
+

Polygon segmentation

@@ -95,109 +95,130 @@ export function SegmentationLab({ Refresh models
- {loadingSegmentationModels ? ( -
- Loading segmentation models. -

Checking backend model registry availability.

+ +
+
+
+

Model registry

+

Backend-reported segmenter states and limitations.

+
- ) : null} - {segmentationModelError ? ( -
- Segmentation model registry unavailable. -

{segmentationModelError}

-
- ) : null} - {segmentationModels.length === 0 && !loadingSegmentationModels ? ( -
- No segmentation models reported by backend. -

Refresh models after the backend is reachable.

-
- ) : null} -
    - {segmentationModels.map((model) => ( -
  • - {model.display_name} - {model.status} -
    - {model.model_id} - {model.framework} - {model.task_type} +
    + {loadingSegmentationModels ? ( +
    + Loading segmentation models. +

    Checking backend model registry availability.

    -

    classes: {model.supported_classes.join(', ')}

    -

    {model.limitation_message}

    -
  • - ))} -
-
-

Run segmentation

-
- - - + ) : null} + {segmentationModelError ? ( +
+ Segmentation model registry unavailable. +

{segmentationModelError}

+
+ ) : null} + {segmentationModels.length === 0 && !loadingSegmentationModels ? ( +
+ No segmentation models reported by backend. +

Refresh models after the backend is reachable.

+
+ ) : null}
- +
    + {segmentationModels.map((model) => ( +
  • + {model.display_name} + {model.status} +
    + {model.model_id} + {model.framework} + {model.task_type} +
    +

    classes: {model.supported_classes.join(', ')}

    +

    {model.limitation_message}

    +
  • + ))} +
- {!selectedSegmentationModelConfigured ? ( -
- Segmentation model is not ready. -

{selectedSegmentationModelLimitation ?? 'Select a configured segmentation model'}

-
- ) : null} - {segmentationRunError ? ( -
- Segmentation run failed. -

{segmentationRunError}

-
- ) : null} - {segmentationRunResult ? ( -
-

Status: {segmentationRunResult.status}

-

Message: {segmentationRunResult.message}

-

Analysis run: {segmentationRunResult.analysis_run_id}

-

Job: {segmentationRunResult.job_id}

-

Segmentations: {segmentationRunResult.segmentation_count}

- {segmentationRunResult.error_code ?

Code: {segmentationRunResult.error_code}

: null} -
- ) : null} +
+
+

Run segmentation

+
+ + + +
+ +
+
+ +
+ {!selectedSegmentationModelConfigured ? ( +
+ Segmentation model is not ready. +

{selectedSegmentationModelLimitation ?? 'Select a configured segmentation model'}

+
+ ) : null} + {segmentationRunError ? ( +
+ Segmentation run failed. +

{segmentationRunError}

+
+ ) : null} + {segmentationRunResult ? ( +
+

Status: {segmentationRunResult.status}

+

Message: {segmentationRunResult.message}

+

Analysis run: {segmentationRunResult.analysis_run_id}

+

Job: {segmentationRunResult.job_id}

+

Segmentations: {segmentationRunResult.segmentation_count}

+ {segmentationRunResult.error_code ?

Code: {segmentationRunResult.error_code}

: null} +
+ ) : null} +
+ +
-

Segmentation results

+
+

Segmentation results

+

Load persisted segmentation polygons and filter by class or confidence.

+
@@ -244,9 +265,11 @@ export function SegmentationLab({

Retrieving persisted segmentation polygons for the selected run.

) : null} -
- Segmentations loaded: {segmentationItems.length} -

{selectedSegmentationRunId ? 'Loaded from persisted segmentation records.' : 'Select a segmentation run before loading results.'}

+
+
+ Segmentations loaded: {segmentationItems.length} +

{selectedSegmentationRunId ? 'Loaded from persisted segmentation records.' : 'Select a segmentation run before loading results.'}

+
{segmentationItems.length > 0 ? (
@@ -277,7 +300,8 @@ export function SegmentationLab({
) : null}
-
+ +

Segmentation QA