feat(lab): standardize evidence workspace template

This commit is contained in:
DCCONSTRUCTIONS
2026-07-26 17:06:13 +03:00
parent 360ec39ff3
commit e9ff29297b
4 changed files with 215 additions and 45 deletions
+2
View File
@@ -315,6 +315,8 @@ export default function App() {
const observationFullscreenActive = Boolean(
activeDefinition?.kind === "spatial"
? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
: activeDefinition?.kind === "lab-archive"
? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
: activeDefinition?.kind === "cameras" && focusedObservationSource,
);
+45 -4
View File
@@ -2385,7 +2385,8 @@
.recordings-workspace,
.lab-archive-workspace,
.lab-result-surface {
.lab-result-surface,
.laboratory-work-template {
display: grid;
gap: 0.85rem;
}
@@ -2542,12 +2543,52 @@
white-space: nowrap;
}
.lab-result-surface {
.laboratory-evidence-frame {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
border-radius: 1rem;
}
.lab-result-surface[data-resizable="true"] .laboratory-evidence-frame {
height: clamp(42rem, 68vh, 58rem);
min-height: 42rem;
resize: vertical;
}
.laboratory-evidence-frame > .spatial-workspace {
height: 100%;
min-height: 0;
}
.lab-result-surface .spatial-workspace {
min-height: 34rem;
.laboratory-evidence-frame > .lidar-quality-workspace {
min-height: 42rem;
}
.lab-archive-workspace[data-viewer-focused="true"] {
height: 100%;
min-height: 0;
padding: 0;
}
.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-selector,
.lab-archive-workspace[data-viewer-focused="true"]
.laboratory-work-template > :not(.lab-result-surface),
.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface > header {
display: none;
}
.lab-archive-workspace[data-viewer-focused="true"] > .laboratory-work-output,
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-work-template,
.lab-archive-workspace[data-viewer-focused="true"] .lab-result-surface,
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame {
height: 100%;
min-height: 0;
}
.lab-archive-workspace[data-viewer-focused="true"] .laboratory-evidence-frame {
resize: none;
}
.laboratory-result-pending {
@@ -5,6 +5,7 @@ import {
useRef,
useState,
type ComponentType,
type ReactNode,
} from "react";
import {
Button,
@@ -1281,6 +1282,54 @@ function LaboratoryTask({
);
}
function LaboratoryEvidence({
eyebrow,
title,
resizable = false,
children,
}: {
eyebrow: string;
title: string;
resizable?: boolean;
children: ReactNode;
}) {
return (
<section
className="lab-result-surface"
data-resizable={resizable ? "true" : undefined}
>
<header>
<span className="section-eyebrow">{eyebrow}</span>
<strong>{title}</strong>
</header>
<div className="laboratory-evidence-frame">
{children}
</div>
</section>
);
}
function LaboratoryWorkTemplate({
task,
evidence,
result = null,
details = null,
}: {
task: ReactNode;
evidence: ReactNode;
result?: ReactNode;
details?: ReactNode;
}) {
return (
<div className="laboratory-work-template">
{task}
{evidence}
{result}
{details}
</div>
);
}
function formatSeconds(value: number): string {
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
}
@@ -1334,8 +1383,9 @@ function E29LaboratoryResult({
}, [result.resultId, selectedFrameIndex]);
return (
<>
<LaboratoryTask
<LaboratoryWorkTemplate
task={(
<LaboratoryTask
title="LAB E29 · camera-first semantics + независимая геометрия"
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
status="Проверенные артефакты"
@@ -1354,13 +1404,14 @@ function E29LaboratoryResult({
value: "Control plane · read-only · hash verified",
},
]}
/>
<section className="lab-result-surface">
<header>
<span className="section-eyebrow">ИСХОДНЫЕ ДАННЫЕ</span>
<strong>LiDAR, траектория и камера RAVNOVES00</strong>
</header>
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ИСХОДНЫЕ ДАННЫЕ"
title="LiDAR, траектория и камера RAVNOVES00"
resizable
>
{replayReady ? (
<SpatialWorkspace {...props} />
) : (
@@ -1374,9 +1425,10 @@ function E29LaboratoryResult({
</p>
</div>
)}
</section>
<section className="laboratory-result-summary">
</LaboratoryEvidence>
)}
result={(
<section className="laboratory-result-summary">
<header>
<div>
<span className="section-eyebrow">РЕЗУЛЬТАТ И ВЫВОД</span>
@@ -1409,9 +1461,10 @@ function E29LaboratoryResult({
</div>
</div>
<p>{result.decision.nextGate}</p>
</section>
<section className="laboratory-frame-review">
</section>
)}
details={(
<section className="laboratory-frame-review">
<header>
<div>
<span className="section-eyebrow">КАДРЫ С КОНФЛИКТОМ</span>
@@ -1475,8 +1528,9 @@ function E29LaboratoryResult({
</div>
</div>
)}
</section>
</>
</section>
)}
/>
);
}
@@ -1524,8 +1578,9 @@ function PublishedLaboratoryResult({
const replayReady = props.recordedReplay?.sessionId === session.id;
return (
<>
<LaboratoryTask
<LaboratoryWorkTemplate
task={(
<LaboratoryTask
title={`${lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`}
description="Опубликованная лабораторная работа открывается по своей неизменяемой записи. Viewer показывает исходные синхронные каналы; за ним приведены только метрики, зафиксированные в LAB-provenance этого результата."
status="Зафиксированный результат"
@@ -1535,13 +1590,14 @@ function PublishedLaboratoryResult({
{ label: "Конфигурация", value: lab?.configSha256?.slice(0, 16) ?? "Не зафиксирована" },
{ label: "Полномочия", value: "Диагностика · без навигации и команд" },
]}
/>
<section className="lab-result-surface">
<header>
<span className="section-eyebrow">ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО</span>
<strong>Исходная запись выбранной лабораторной работы</strong>
</header>
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="Исходная запись выбранной лабораторной работы"
resizable
>
{replayReady ? (
<SpatialWorkspace {...props} />
) : (
@@ -1555,9 +1611,10 @@ function PublishedLaboratoryResult({
</p>
</div>
)}
</section>
<section className="laboratory-result-summary">
</LaboratoryEvidence>
)}
result={(
<section className="laboratory-result-summary">
<header>
<div>
<span className="section-eyebrow">ЗАФИКСИРОВАННЫЕ МЕТРИКИ</span>
@@ -1584,8 +1641,9 @@ function PublishedLaboratoryResult({
</div>
)}
</div>
</section>
</>
</section>
)}
/>
);
}
@@ -1792,8 +1850,16 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
);
}
const viewerFocused = Boolean(
props.observationLayout.focusedSourceId
|| props.observationLayout.maximizedFloatingSourceId,
);
return (
<div className="lab-archive-workspace">
<div
className="lab-archive-workspace"
data-viewer-focused={viewerFocused ? "true" : undefined}
>
<LaboratorySelector
eyebrow="ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА"
title={profiles.find((profile) => profile.id === profileId)?.label ?? rigLabel}
@@ -1817,8 +1883,9 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
<div className="laboratory-work-output">
{workId === "e28-local-surface" ? (
<>
<LaboratoryTask
<LaboratoryWorkTemplate
task={(
<LaboratoryTask
title="LAB E28 · локальная модель поверхности L2.6"
description="Полная запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает локальную поверхность, наблюдаемые препятствия, временные скачки и ошибки предсказания без изменения исходного сенсора и без командного канала."
status="Проверенные артефакты"
@@ -1831,13 +1898,21 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
{ label: "Режим", value: "Recorded-source-paced shadow" },
{ label: "Артефакты", value: "Control plane · read-only · hash verified" },
]}
/>
<LidarQualityWorkspace
embedded
deviceLabel={props.deviceLabel}
onOpenObservation={() => props.navigation.openView("spatial-scene")}
/>
</>
/>
)}
evidence={(
<LaboratoryEvidence
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
title="Диагностическая поверхность и кадры LAB E28"
>
<LidarQualityWorkspace
embedded
deviceLabel={props.deviceLabel}
onOpenObservation={() => props.navigation.openView("spatial-scene")}
/>
</LaboratoryEvidence>
)}
/>
) : workId === "e29-camera-geometry" && e29Result && e29SourceSession ? (
<E29LaboratoryResult
props={props}
@@ -0,0 +1,52 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let productModel;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
productModel = await server.ssrLoadModule("/src/productModel.ts");
});
after(async () => {
await server?.close();
});
test("top navigation has no Center and Park owns contour health first", () => {
assert.deepEqual(
productModel.roots.map((root) => root.id),
["fleet", "observation", "missions", "data", "system", "polygon"],
);
assert.equal(productModel.workspacesForRoot("fleet")[0]?.id, "contour-health");
assert.equal(productModel.workspaceById("contour-health")?.root, "fleet");
});
test("every laboratory result uses the shared evidence template", async () => {
const source = await readFile(
new URL("../src/workspaces/Workspaces.tsx", import.meta.url),
"utf8",
);
const css = await readFile(
new URL("../src/styles/workspaces.css", import.meta.url),
"utf8",
);
const app = await readFile(
new URL("../src/App.tsx", import.meta.url),
"utf8",
);
assert.match(source, /function LaboratoryWorkTemplate\(/);
assert.match(source, /function LaboratoryEvidence\(/);
assert.match(source, /data-viewer-focused=/);
assert.match(css, /height:\s*clamp\(42rem,\s*68vh,\s*58rem\)/);
assert.match(css, /resize:\s*vertical/);
assert.match(app, /activeDefinition\?\.kind === "lab-archive"/);
});