Harden detection result review
This commit is contained in:
@@ -155,6 +155,13 @@ AI Lab run controls explicitly explain when no raster dataset is available, inst
|
||||
- `App.tsx` still owns shared state orchestration and API calls; extracted components receive the same state and callbacks as props.
|
||||
- Existing MapLibre overlay behavior, dataset/reference flows, Detection Lab flows and Segmentation Lab flows are unchanged.
|
||||
|
||||
## Detection result scale and review
|
||||
|
||||
- Detection Lab keeps the complete persisted detection collection available to the existing MapLibre overlay and QA/QC flows.
|
||||
- The results table renders 50 rows by default and provides 25/50/100 row sizes plus previous/next controls. This bounds DOM work for dense AOIs without discarding or resampling detections.
|
||||
- Selecting another run, changing a class/confidence filter or loading a new result collection resets the visible table to page one.
|
||||
- Pagination is intentionally client-side over the canonical persisted response; detection API contracts and GeoJSON output are unchanged.
|
||||
|
||||
## Sprint 15 additions
|
||||
|
||||
- Added a Projects panel action to load the explicit offline demo workflow.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type {
|
||||
DatasetCreateResponse,
|
||||
DetectionModelCapability,
|
||||
@@ -12,6 +13,9 @@ import type {
|
||||
import type { DetectionCalibrationRunRow } from '../../hooks/useDetectionWorkflow'
|
||||
import { DETECTION_OPERATOR_PROFILES, type DetectionOperatorProfile } from './detectionProfiles'
|
||||
|
||||
const DETECTION_PAGE_SIZE_OPTIONS = [25, 50, 100] as const
|
||||
const DEFAULT_DETECTION_PAGE_SIZE = 50
|
||||
|
||||
interface CalibrationRow {
|
||||
analysisRunId: string
|
||||
qualityCheckId: string
|
||||
@@ -152,6 +156,18 @@ export function DetectionLab({
|
||||
const bestF1Candidate = bestCalibrationRow(calibrationRows, 'f1')
|
||||
const bestPrecisionCandidate = bestCalibrationRow(calibrationRows, 'precision')
|
||||
const lowestFalsePositivePressureCandidate = bestLowestCalibrationRow(calibrationRows, 'falsePositives')
|
||||
const [detectionResultPage, setDetectionResultPage] = useState(1)
|
||||
const [detectionPageSize, setDetectionPageSize] = useState(DEFAULT_DETECTION_PAGE_SIZE)
|
||||
const detectionPageCount = Math.max(1, Math.ceil(detectionItems.length / detectionPageSize))
|
||||
const currentDetectionPage = Math.min(detectionResultPage, detectionPageCount)
|
||||
const detectionPageStart = (currentDetectionPage - 1) * detectionPageSize
|
||||
const detectionPageEnd = Math.min(detectionPageStart + detectionPageSize, detectionItems.length)
|
||||
const visibleDetectionItems = detectionItems.slice(detectionPageStart, detectionPageEnd)
|
||||
|
||||
useEffect(() => {
|
||||
setDetectionResultPage(1)
|
||||
}, [selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter, detectionItems])
|
||||
|
||||
const detectionHasTileManifest =
|
||||
!detectionRequiresTileManifest || detectionTileManifestPath.trim().length > 0
|
||||
const detectionRunReady =
|
||||
@@ -744,28 +760,73 @@ export function DetectionLab({
|
||||
</div>
|
||||
</div>
|
||||
{detectionItems.length > 0 ? (
|
||||
<div className="table-scroll">
|
||||
<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="pagination-toolbar" aria-label="Detection result pagination">
|
||||
<p className="pagination-summary" aria-live="polite">
|
||||
<strong>{detectionPageStart + 1}-{detectionPageEnd}</strong>
|
||||
<span>of {detectionItems.length}</span>
|
||||
</p>
|
||||
<label className="pagination-page-size">
|
||||
Rows
|
||||
<select
|
||||
value={detectionPageSize}
|
||||
onChange={(event) => {
|
||||
setDetectionPageSize(Number(event.target.value))
|
||||
setDetectionResultPage(1)
|
||||
}}
|
||||
>
|
||||
{DETECTION_PAGE_SIZE_OPTIONS.map((pageSize) => (
|
||||
<option key={pageSize} value={pageSize}>{pageSize}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<div className="pagination-actions">
|
||||
<button
|
||||
className="secondary-action pagination-button"
|
||||
type="button"
|
||||
aria-label="Previous detection results page"
|
||||
title="Previous page"
|
||||
disabled={currentDetectionPage <= 1}
|
||||
onClick={() => setDetectionResultPage(currentDetectionPage - 1)}
|
||||
>
|
||||
{'<'}
|
||||
</button>
|
||||
<span>Page {currentDetectionPage} of {detectionPageCount}</span>
|
||||
<button
|
||||
className="secondary-action pagination-button"
|
||||
type="button"
|
||||
aria-label="Next detection results page"
|
||||
title="Next page"
|
||||
disabled={currentDetectionPage >= detectionPageCount}
|
||||
onClick={() => setDetectionResultPage(currentDetectionPage + 1)}
|
||||
>
|
||||
{'>'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="table-scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Class</th>
|
||||
<th>Confidence</th>
|
||||
<th>Model</th>
|
||||
<th>Source tile</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleDetectionItems.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>
|
||||
|
||||
|
||||
@@ -998,6 +998,51 @@ section th {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.pagination-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem 1rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 0.85rem;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 7px;
|
||||
padding: 0.55rem 0.65rem;
|
||||
background: #f8fbf9;
|
||||
}
|
||||
|
||||
.pagination-summary,
|
||||
.pagination-actions,
|
||||
.pagination-page-size {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pagination-summary span,
|
||||
.pagination-actions span,
|
||||
.pagination-page-size {
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.pagination-page-size select {
|
||||
width: auto;
|
||||
min-width: 4.5rem;
|
||||
margin: 0;
|
||||
padding: 0.35rem 1.65rem 0.35rem 0.45rem;
|
||||
}
|
||||
|
||||
.pagination-button {
|
||||
width: 2rem;
|
||||
min-width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.workbench-inspector .job-result,
|
||||
.workbench-inspector pre {
|
||||
max-height: 16rem;
|
||||
|
||||
Reference in New Issue
Block a user