feat(lab): standardize evidence workspace template
This commit is contained in:
@@ -315,6 +315,8 @@ export default function App() {
|
|||||||
const observationFullscreenActive = Boolean(
|
const observationFullscreenActive = Boolean(
|
||||||
activeDefinition?.kind === "spatial"
|
activeDefinition?.kind === "spatial"
|
||||||
? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
|
? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
|
||||||
|
: activeDefinition?.kind === "lab-archive"
|
||||||
|
? observationLayout.maximizedFloatingSourceId || focusedObservationSource?.modality === "point-cloud"
|
||||||
: activeDefinition?.kind === "cameras" && focusedObservationSource,
|
: activeDefinition?.kind === "cameras" && focusedObservationSource,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2385,7 +2385,8 @@
|
|||||||
|
|
||||||
.recordings-workspace,
|
.recordings-workspace,
|
||||||
.lab-archive-workspace,
|
.lab-archive-workspace,
|
||||||
.lab-result-surface {
|
.lab-result-surface,
|
||||||
|
.laboratory-work-template {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.85rem;
|
gap: 0.85rem;
|
||||||
}
|
}
|
||||||
@@ -2542,12 +2543,52 @@
|
|||||||
white-space: nowrap;
|
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;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lab-result-surface .spatial-workspace {
|
.laboratory-evidence-frame > .lidar-quality-workspace {
|
||||||
min-height: 34rem;
|
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 {
|
.laboratory-result-pending {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
type ComponentType,
|
type ComponentType,
|
||||||
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
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 {
|
function formatSeconds(value: number): string {
|
||||||
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
|
return `${value.toLocaleString("ru-RU", { maximumFractionDigits: 3 })} с`;
|
||||||
}
|
}
|
||||||
@@ -1334,7 +1383,8 @@ function E29LaboratoryResult({
|
|||||||
}, [result.resultId, selectedFrameIndex]);
|
}, [result.resultId, selectedFrameIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<LaboratoryWorkTemplate
|
||||||
|
task={(
|
||||||
<LaboratoryTask
|
<LaboratoryTask
|
||||||
title="LAB E29 · camera-first semantics + независимая геометрия"
|
title="LAB E29 · camera-first semantics + независимая геометрия"
|
||||||
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
|
description="Проверяется рабочая продуктовая граница: камера сохраняет семантический класс и идентичность объекта, LiDAR подтверждает метрическую дальность и занятую геометрию по локальной поверхности L2.6. Отсутствие точек не объявляется свободным пространством."
|
||||||
@@ -1355,12 +1405,13 @@ function E29LaboratoryResult({
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<section className="lab-result-surface">
|
evidence={(
|
||||||
<header>
|
<LaboratoryEvidence
|
||||||
<span className="section-eyebrow">ИСХОДНЫЕ ДАННЫЕ</span>
|
eyebrow="ИСХОДНЫЕ ДАННЫЕ"
|
||||||
<strong>LiDAR, траектория и камера RAVNOVES00</strong>
|
title="LiDAR, траектория и камера RAVNOVES00"
|
||||||
</header>
|
resizable
|
||||||
|
>
|
||||||
{replayReady ? (
|
{replayReady ? (
|
||||||
<SpatialWorkspace {...props} />
|
<SpatialWorkspace {...props} />
|
||||||
) : (
|
) : (
|
||||||
@@ -1374,8 +1425,9 @@ function E29LaboratoryResult({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
<section className="laboratory-result-summary">
|
<section className="laboratory-result-summary">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
@@ -1410,7 +1462,8 @@ function E29LaboratoryResult({
|
|||||||
</div>
|
</div>
|
||||||
<p>{result.decision.nextGate}</p>
|
<p>{result.decision.nextGate}</p>
|
||||||
</section>
|
</section>
|
||||||
|
)}
|
||||||
|
details={(
|
||||||
<section className="laboratory-frame-review">
|
<section className="laboratory-frame-review">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
@@ -1476,7 +1529,8 @@ function E29LaboratoryResult({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</>
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,7 +1578,8 @@ function PublishedLaboratoryResult({
|
|||||||
const replayReady = props.recordedReplay?.sessionId === session.id;
|
const replayReady = props.recordedReplay?.sessionId === session.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<LaboratoryWorkTemplate
|
||||||
|
task={(
|
||||||
<LaboratoryTask
|
<LaboratoryTask
|
||||||
title={`${lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`}
|
title={`${lab?.labId ?? "LAB"} · ${laboratorySessionTitle(session)}`}
|
||||||
description="Опубликованная лабораторная работа открывается по своей неизменяемой записи. Viewer показывает исходные синхронные каналы; за ним приведены только метрики, зафиксированные в LAB-provenance этого результата."
|
description="Опубликованная лабораторная работа открывается по своей неизменяемой записи. Viewer показывает исходные синхронные каналы; за ним приведены только метрики, зафиксированные в LAB-provenance этого результата."
|
||||||
@@ -1536,12 +1591,13 @@ function PublishedLaboratoryResult({
|
|||||||
{ label: "Полномочия", value: "Диагностика · без навигации и команд" },
|
{ label: "Полномочия", value: "Диагностика · без навигации и команд" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<section className="lab-result-surface">
|
evidence={(
|
||||||
<header>
|
<LaboratoryEvidence
|
||||||
<span className="section-eyebrow">ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО</span>
|
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||||
<strong>Исходная запись выбранной лабораторной работы</strong>
|
title="Исходная запись выбранной лабораторной работы"
|
||||||
</header>
|
resizable
|
||||||
|
>
|
||||||
{replayReady ? (
|
{replayReady ? (
|
||||||
<SpatialWorkspace {...props} />
|
<SpatialWorkspace {...props} />
|
||||||
) : (
|
) : (
|
||||||
@@ -1555,8 +1611,9 @@ function PublishedLaboratoryResult({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
result={(
|
||||||
<section className="laboratory-result-summary">
|
<section className="laboratory-result-summary">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
@@ -1585,7 +1642,8 @@ function PublishedLaboratoryResult({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1792,8 +1850,16 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const viewerFocused = Boolean(
|
||||||
|
props.observationLayout.focusedSourceId
|
||||||
|
|| props.observationLayout.maximizedFloatingSourceId,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="lab-archive-workspace">
|
<div
|
||||||
|
className="lab-archive-workspace"
|
||||||
|
data-viewer-focused={viewerFocused ? "true" : undefined}
|
||||||
|
>
|
||||||
<LaboratorySelector
|
<LaboratorySelector
|
||||||
eyebrow="ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА"
|
eyebrow="ПРОФИЛЬ ЛАБОРАТОРНОГО КОНТУРА"
|
||||||
title={profiles.find((profile) => profile.id === profileId)?.label ?? rigLabel}
|
title={profiles.find((profile) => profile.id === profileId)?.label ?? rigLabel}
|
||||||
@@ -1817,7 +1883,8 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
|
|||||||
|
|
||||||
<div className="laboratory-work-output">
|
<div className="laboratory-work-output">
|
||||||
{workId === "e28-local-surface" ? (
|
{workId === "e28-local-surface" ? (
|
||||||
<>
|
<LaboratoryWorkTemplate
|
||||||
|
task={(
|
||||||
<LaboratoryTask
|
<LaboratoryTask
|
||||||
title="LAB E28 · локальная модель поверхности L2.6"
|
title="LAB E28 · локальная модель поверхности L2.6"
|
||||||
description="Полная запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает локальную поверхность, наблюдаемые препятствия, временные скачки и ошибки предсказания без изменения исходного сенсора и без командного канала."
|
description="Полная запись RAVNOVES00 воспроизводится через bounded shadow-контур. Модель оценивает локальную поверхность, наблюдаемые препятствия, временные скачки и ошибки предсказания без изменения исходного сенсора и без командного канала."
|
||||||
@@ -1832,12 +1899,20 @@ function LabArchiveWorkspace(props: WorkspaceRendererProps) {
|
|||||||
{ label: "Артефакты", value: "Control plane · read-only · hash verified" },
|
{ label: "Артефакты", value: "Control plane · read-only · hash verified" },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
evidence={(
|
||||||
|
<LaboratoryEvidence
|
||||||
|
eyebrow="ВИЗУАЛЬНОЕ ДОКАЗАТЕЛЬСТВО"
|
||||||
|
title="Диагностическая поверхность и кадры LAB E28"
|
||||||
|
>
|
||||||
<LidarQualityWorkspace
|
<LidarQualityWorkspace
|
||||||
embedded
|
embedded
|
||||||
deviceLabel={props.deviceLabel}
|
deviceLabel={props.deviceLabel}
|
||||||
onOpenObservation={() => props.navigation.openView("spatial-scene")}
|
onOpenObservation={() => props.navigation.openView("spatial-scene")}
|
||||||
/>
|
/>
|
||||||
</>
|
</LaboratoryEvidence>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
) : workId === "e29-camera-geometry" && e29Result && e29SourceSession ? (
|
) : workId === "e29-camera-geometry" && e29Result && e29SourceSession ? (
|
||||||
<E29LaboratoryResult
|
<E29LaboratoryResult
|
||||||
props={props}
|
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