Add calibration summary export
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-07-08 13:31:11 +02:00
parent 682a113d64
commit 8089df3504
6 changed files with 120 additions and 1 deletions
@@ -571,7 +571,17 @@ export function DetectionLab({
<h3>Calibration run progress</h3>
<p className="muted">Each row is backed by a persisted detection run and QA check when successful.</p>
</div>
<span className="count-pill">{detectionCalibrationRows.length} thresholds</span>
<div className="panel-action-row">
<span className="count-pill">{detectionCalibrationRows.length} thresholds</span>
<button
className="secondary-action"
type="button"
onClick={() => downloadCalibrationSummary(selectedProjectId, detectionCalibrationRows)}
disabled={detectionCalibrationRows.length === 0 || !selectedProjectId}
>
Download calibration summary
</button>
</div>
</div>
<div className="table-scroll">
<table>
@@ -940,6 +950,51 @@ function stringFromRecord(record: Record<string, unknown> | null | undefined, ke
return typeof value === 'string' && value.trim().length > 0 ? value : null
}
function buildCalibrationSummaryExport(projectId: string, rows: DetectionCalibrationRunRow[]): Record<string, unknown> {
const successfulRows = rows.filter((row) => row.status === 'success')
return {
export_type: 'detection_calibration_summary',
project_id: projectId,
created_at: new Date().toISOString(),
calibration_thresholds: rows.map((row) => row.threshold),
quality_check_ids: successfulRows.map((row) => row.quality_check_id).filter(Boolean),
rows: rows.map((row) => ({
threshold: row.threshold,
status: row.status,
analysis_run_id: row.analysis_run_id ?? null,
job_id: row.job_id ?? null,
quality_check_id: row.quality_check_id ?? null,
evidence_geojson_url: row.quality_check_id
? `/api/v1/projects/${projectId}/quality-checks/${row.quality_check_id}/evidence/geojson`
: null,
detection_count: row.detection_count ?? null,
precision: row.precision ?? null,
recall: row.recall ?? null,
f1_score: row.f1_score ?? null,
false_positives: row.false_positives ?? null,
false_negatives: row.false_negatives ?? null,
message: row.message ?? null,
})),
}
}
function downloadCalibrationSummary(projectId: string | null, rows: DetectionCalibrationRunRow[]): void {
if (!projectId || rows.length === 0) {
return
}
downloadJsonFile('detection-calibration-summary.json', buildCalibrationSummaryExport(projectId, rows))
}
function downloadJsonFile(filename: string, payload: unknown): void {
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
function formatNullableNumber(value: number | null, digits: number): string {
return typeof value === 'number' && Number.isFinite(value) ? value.toFixed(digits) : 'n/a'
}
+8
View File
@@ -3315,6 +3315,14 @@ button.entity-card {
white-space: nowrap;
}
.panel-action-row {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
}
.calibration-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));