feat(lab): standardize evidence workspace template
This commit is contained in:
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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"/);
|
||||
});
|
||||
Reference in New Issue
Block a user