Harden detection result review
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-13 13:31:43 +02:00
parent 6fe1bbd946
commit 4455e242c6
12 changed files with 795 additions and 22 deletions
@@ -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>
+45
View File
@@ -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;