feat: add presentation demo mode
This commit is contained in:
90
README.md
90
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
Инженерный frontend MVP для задачи хакатона OZON Tech: «Интеллектуальная роботизированная система сортировки товаров».
|
||||
|
||||
Проект показывает 2D/SVG dashboard сортировочной линии: рабочую зону 6000 x 10000 мм, конвейер шириной 500 мм, зоны A/B/C/D, roll-cage C/D, датчики, stop-gate, толкатели, классификацию, маршрутизацию, timeline цикла, PID-имитацию, метрики и event log.
|
||||
Проект показывает 2D/SVG dashboard сортировочной линии: рабочую зону 6000 x 10000 мм, конвейер шириной 500 мм, зоны A/B/C/D, roll-cage C/D, датчики, stop-gate, толкатели, классификацию, маршрутизацию, timeline цикла, PID-имитацию, метрики, event log и режим защиты для жюри.
|
||||
|
||||
## Стек
|
||||
|
||||
@@ -15,6 +15,81 @@
|
||||
|
||||
Нет Three.js, backend, реального ML и тяжелых UI-библиотек.
|
||||
|
||||
## Как открыть демо
|
||||
|
||||
Публично:
|
||||
|
||||
```text
|
||||
https://arhipovdan.ru/
|
||||
https://www.arhipovdan.ru/
|
||||
```
|
||||
|
||||
Локально на сервере:
|
||||
|
||||
```text
|
||||
http://127.0.0.1:3100/
|
||||
```
|
||||
|
||||
## Presentation Mode
|
||||
|
||||
В Header есть переключатель:
|
||||
|
||||
- `Engineering Mode` — полный инженерный dashboard.
|
||||
- `Presentation Mode` — режим защиты с demo narrative, подсказками докладчика, фокусом на текущую область и criteria checklist.
|
||||
|
||||
В Presentation Mode панель demo steps показывает:
|
||||
|
||||
- номер текущего шага;
|
||||
- что сейчас происходит;
|
||||
- что смотреть на экране;
|
||||
- что это доказывает для жюри;
|
||||
- связанные OZON criteria;
|
||||
- готовую presenter phrase.
|
||||
|
||||
Кнопки:
|
||||
|
||||
- `Previous step` / `Next step`;
|
||||
- `Restart demo`;
|
||||
- `Apply scenario`;
|
||||
- `Run suggested action`.
|
||||
|
||||
Для `jam` и `emergency_stop` при `Safe Demo: ON` требуется явное повторное подтверждение fault-сценария.
|
||||
|
||||
## Demo Steps
|
||||
|
||||
1. System overview.
|
||||
2. Normal item to B.
|
||||
3. Oversized item to C.
|
||||
4. Round object to D.
|
||||
5. Boundary dimensions.
|
||||
6. Low confidence fallback.
|
||||
7. Close items queue.
|
||||
8. Jam / fault handling.
|
||||
9. Emergency stop.
|
||||
10. Performance and synchronization.
|
||||
|
||||
## OZON Criteria Coverage Panel
|
||||
|
||||
Панель `OZON criteria coverage` показывает, какие критерии закрыты и где это доказано:
|
||||
|
||||
- category correctness;
|
||||
- classification rules;
|
||||
- boundary cases;
|
||||
- physical routing;
|
||||
- manipulation logic;
|
||||
- geometry variety;
|
||||
- safety;
|
||||
- timing and throughput;
|
||||
- CV-to-actuator integration;
|
||||
- engineering realism;
|
||||
- reproducibility/docs.
|
||||
|
||||
Фильтры: `all`, `covered`, `partially covered`, `demo step available`.
|
||||
|
||||
## Event Log JSON
|
||||
|
||||
`Copy JSON` в Event Log использует browser clipboard API. Если clipboard недоступен в браузере, экспорт не критичен для защиты: используйте визуальный Event Log с фильтрами, timestamp, state, command/category и severity. DevTools открывать не требуется.
|
||||
|
||||
## Запуск локально
|
||||
|
||||
```bash
|
||||
@@ -22,8 +97,6 @@ npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Открыть: http://127.0.0.1:3100/
|
||||
|
||||
## Запуск через Docker
|
||||
|
||||
```bash
|
||||
@@ -90,13 +163,7 @@ State machine управляет циклом:
|
||||
|
||||
## Simplified PID
|
||||
|
||||
PID-панель показывает упрощенную имитацию control loop:
|
||||
|
||||
- target speed;
|
||||
- actual speed;
|
||||
- error;
|
||||
- correction;
|
||||
- mini graph последних тиков скорости.
|
||||
PID-панель показывает упрощенную имитацию control loop: target speed, actual speed, error, correction и mini graph последних тиков скорости.
|
||||
|
||||
В normal flow actual speed приближается к target. В `jam` и `emergency_stop` target становится 0, actual speed визуально падает к 0.
|
||||
|
||||
@@ -106,7 +173,7 @@ PID-панель показывает упрощенную имитацию cont
|
||||
npm run test
|
||||
```
|
||||
|
||||
Покрыты classifier, PID и целостность сценариев.
|
||||
Покрыты classifier, PID, сценарии, demo steps и OZON criteria.
|
||||
|
||||
## Документация
|
||||
|
||||
@@ -114,6 +181,7 @@ npm run test
|
||||
- `docs/DEMO_SCRIPT.md` — 3-5 минутный сценарий защиты.
|
||||
- `docs/SCENARIOS.md` — ожидаемые результаты сценариев.
|
||||
- `docs/JURY_QA.md` — короткие ответы на вопросы жюри.
|
||||
- `docs/SUBMISSION_CHECKLIST.md` — checklist перед сдачей.
|
||||
|
||||
## Ограничения MVP
|
||||
|
||||
|
||||
@@ -1,69 +1,98 @@
|
||||
# Demo Script
|
||||
|
||||
Target duration: 3-5 minutes.
|
||||
Open https://arhipovdan.ru/ and switch Header to `Presentation Mode`.
|
||||
|
||||
## 1. Open The Dashboard
|
||||
## 3-Minute Defense
|
||||
|
||||
Open https://arhipovdan.ru/.
|
||||
### 0:00-0:25 — System Overview
|
||||
|
||||
Explain that this is a 2D engineering simulation, not a decorative animation: the SVG scene shows a scaled work zone, conveyor dimensions, sensors, stop-gate, pushers and roll-cages.
|
||||
Demo step: `System overview`.
|
||||
|
||||
## 2. Normal Flow
|
||||
Say: the system models the full loop: detection -> classification -> actuator command -> physical route.
|
||||
|
||||
Select `Normal flow`, press `Start`.
|
||||
Watch: engineering SVG scene, sensors, stop-gate, pushers and B/C/D routes.
|
||||
|
||||
Explain:
|
||||
### 0:25-0:55 — Normal Item To B
|
||||
|
||||
- item enters zone A;
|
||||
- camera captures bbox;
|
||||
- laser measures height;
|
||||
- ultrasonic confirms gate position;
|
||||
- gate holds item;
|
||||
- classifier selects B/C/D;
|
||||
- route command is shown on the scene;
|
||||
- event log records the full cycle.
|
||||
Demo step: `Normal item to B`.
|
||||
|
||||
Question closed: can the system show a full sorting cycle end-to-end?
|
||||
Apply scenario, use `Run suggested action` or `Step state`.
|
||||
|
||||
## 3. Step-By-Step Decision
|
||||
Say: dimensions pass, roundness is below threshold, category B opens the gate and sends the item straight.
|
||||
|
||||
Press `Reset`, then use `Step state`.
|
||||
### 0:55-1:25 — Oversized To C
|
||||
|
||||
Explain each state in the timeline. Show that Step advances by logical state, not by arbitrary animation time.
|
||||
Demo step: `Oversized item to C`.
|
||||
|
||||
Question closed: can the jury inspect synchronization and state transitions?
|
||||
Say: dimensions fail first, so C has priority even before shape routing.
|
||||
|
||||
## 4. Oversized And Round Rules
|
||||
Watch: decision tree FAIL, `ROUTE_TO_C`, lower roll-cage.
|
||||
|
||||
Select `Oversized item`, step to classification.
|
||||
### 1:25-1:55 — Round Object To D
|
||||
|
||||
Show decision tree:
|
||||
Demo step: `Round object to D`.
|
||||
|
||||
- dimensions check fails;
|
||||
- category C selected;
|
||||
- roundness is lower priority because dimensions are checked first.
|
||||
Say: dimensions pass, but roundness >= 0.8, so the item goes to D for repack/shape issue.
|
||||
|
||||
Then select `Round object` and show D when dimensions pass and roundness >= 0.8.
|
||||
### 1:55-2:25 — Fault And Safety
|
||||
|
||||
Question closed: how is classification proved?
|
||||
Demo step: `Jam / fault handling` or `Emergency stop`.
|
||||
|
||||
## 5. Robustness Scenarios
|
||||
With Safe Demo ON, confirm the fault scenario intentionally.
|
||||
|
||||
Show `close_items`, `low_confidence`, `jam`, `emergency_stop`.
|
||||
Say: abnormal states stop conveyor motion and require Reset.
|
||||
|
||||
Explain:
|
||||
### 2:25-3:00 — Criteria Coverage
|
||||
|
||||
- close items generate spacing warning and queue length;
|
||||
- low confidence uses rule-based fallback;
|
||||
- jam enters FAULT and stops conveyor;
|
||||
- emergency stop enters EMERGENCY_STOP and requires Reset.
|
||||
Demo step: `Performance and synchronization`, then focus criteria panel.
|
||||
|
||||
Question closed: what happens outside the happy path?
|
||||
Say: this checklist maps every OZON criterion to scenario, component and doc evidence.
|
||||
|
||||
## 6. PID And Metrics
|
||||
## 5-Minute Defense
|
||||
|
||||
Point to PID panel and metrics cards.
|
||||
Use all 10 demo steps:
|
||||
|
||||
Explain that PID is simplified: actual speed approaches target in normal flow and decays toward zero in fault/emergency.
|
||||
1. System overview.
|
||||
2. Normal item to B.
|
||||
3. Oversized item to C.
|
||||
4. Round object to D.
|
||||
5. Boundary dimensions.
|
||||
6. Low confidence fallback.
|
||||
7. Close items queue.
|
||||
8. Jam / fault handling.
|
||||
9. Emergency stop.
|
||||
10. Performance and synchronization.
|
||||
|
||||
Question closed: how is conveyor control represented without overbuilding physics?
|
||||
For each step:
|
||||
|
||||
1. Read `What to watch`.
|
||||
2. Apply recommended scenario.
|
||||
3. Use `Step state` or `Run suggested action`.
|
||||
4. Point to the highlighted focus area.
|
||||
5. Close with the presenter phrase.
|
||||
|
||||
## Fallback If Domain Does Not Open
|
||||
|
||||
1. SSH to server.
|
||||
2. Check local frontend:
|
||||
|
||||
```bash
|
||||
curl -I http://127.0.0.1:3100/
|
||||
```
|
||||
|
||||
3. Check Docker project:
|
||||
|
||||
```bash
|
||||
docker compose -p owl -f /opt/arhipovdan/app/docker-compose.server.yml ps
|
||||
```
|
||||
|
||||
4. If screenshots/video are prepared later, show them while explaining the same demo steps.
|
||||
|
||||
## What Each Screen Proves
|
||||
|
||||
- SVG scene proves physical route and actuator command.
|
||||
- Classification panel proves why category B/C/D was selected.
|
||||
- Timeline proves synchronization and step-by-step behavior.
|
||||
- PID panel proves conveyor control-loop behavior.
|
||||
- Event Log proves traceability.
|
||||
- Criteria panel proves coverage of OZON evaluation points.
|
||||
|
||||
@@ -1,41 +1,61 @@
|
||||
# Jury Q&A
|
||||
|
||||
## 1. Why 2D, Not 3D?
|
||||
## 1. Why Is This Not Just An Animation?
|
||||
|
||||
The animation is driven by a deterministic state machine, classifier, sensor state, actuator state, metrics and event log. The scene visualizes domain state instead of playing a decorative timeline.
|
||||
|
||||
## 2. How Is Classification Connected To The Actuator?
|
||||
|
||||
`classifyItem` returns B/C/D. The simulation maps that category to `ROUTE_TO_B`, `ROUTE_TO_C` or `ROUTE_TO_D`, which changes gate and pusher states and draws the physical route.
|
||||
|
||||
## 3. How Do You Prove The Route Was Correct?
|
||||
|
||||
The decision tree shows thresholds and PASS/FAIL, the scene shows the selected command and route arrow, and Event Log records state, command/category and item id.
|
||||
|
||||
## 4. Where Are OZON Rules Reflected?
|
||||
|
||||
The rules are explicit in classification limits: min/max dimensions, roundness threshold and C priority. Criteria Coverage maps these rules to scenarios and docs.
|
||||
|
||||
## 5. How Does The Operator Safely Stop The System?
|
||||
|
||||
The `emergency_stop` scenario enters `EMERGENCY_STOP`, stops conveyor target speed and requires Reset. In Presentation Mode, Safe Demo requires confirmation before fault scenarios.
|
||||
|
||||
## 6. What Happens On Sensor Failure Or Low Confidence?
|
||||
|
||||
Low confidence is logged as a warning. The system still applies deterministic dimensions/roundness rules as a fallback and shows this in the panel.
|
||||
|
||||
## 7. Why 2D, Not 3D?
|
||||
|
||||
2D is enough for MVP validation: it shows geometry, timing, sensor positions, routes and state transitions without spending effort on heavy rendering.
|
||||
|
||||
## 2. Where Is Computer Vision?
|
||||
## 8. What Is Needed For A Physical Prototype?
|
||||
|
||||
Replace pseudo-CV with a real CV service, connect PLC/robot telemetry, calibrate sensor latencies, add hardware interlocks and persist event logs.
|
||||
|
||||
## 9. Where Is Computer Vision?
|
||||
|
||||
The MVP uses pseudo-CV: bbox, dimensions, confidence and latency are derived from mock items. The architecture keeps CV output separate from classification, so a real CV service can replace it later.
|
||||
|
||||
## 3. How Is Classification Correctness Proven?
|
||||
## 10. How Is Classification Correctness Proven?
|
||||
|
||||
The decision tree shows PASS/FAIL for dimensions and roundness, actual values, thresholds and final category. Tests cover key boundary cases.
|
||||
|
||||
## 4. How Are Dimensions And Circular Section Handled?
|
||||
## 11. How Are Dimensions And Circular Section Handled?
|
||||
|
||||
Dimensions are checked first against min/max width, depth and height. If they pass, roundness is checked against threshold 0.8.
|
||||
|
||||
## 5. Why Does C Have Priority Over D?
|
||||
## 12. Why Does C Have Priority Over D?
|
||||
|
||||
Oversized or undersized items are operationally unsafe for the main line and must be diverted first. Therefore dimensions check precedes roundness.
|
||||
|
||||
## 6. How Is Synchronization Shown?
|
||||
## 13. How Is Synchronization Shown?
|
||||
|
||||
Cycle timeline shows state order, simulated timestamps, durations and status: done, active, pending or skipped.
|
||||
|
||||
## 7. How Does The Actuator Part Work?
|
||||
## 14. How Does The Actuator Part Work?
|
||||
|
||||
The stop-gate fixes the item. B opens the gate, C extends pusher C, D extends pusher D, then mechanisms return home.
|
||||
|
||||
## 8. What Happens On Jam?
|
||||
## 15. What Happens On Jam?
|
||||
|
||||
The system enters FAULT, conveyor target speed becomes 0, actual speed decays toward 0, and Reset is required.
|
||||
|
||||
## 9. What Happens On Low CV Confidence?
|
||||
|
||||
A warning is logged, but the system still classifies by deterministic dimensions and roundness rules.
|
||||
|
||||
## 10. How To Scale This To A Real Hardware-Software System?
|
||||
|
||||
Replace pseudo-CV with a CV service, connect PLC/robot telemetry via backend/WebSocket, persist event logs, calibrate sensor latencies and add recovery policies.
|
||||
|
||||
19
docs/SUBMISSION_CHECKLIST.md
Normal file
19
docs/SUBMISSION_CHECKLIST.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Submission Checklist
|
||||
|
||||
Before final submission:
|
||||
|
||||
- [ ] `npm run build` passed.
|
||||
- [ ] `npm run test` passed.
|
||||
- [ ] Docker project `owl` is running.
|
||||
- [ ] `https://arhipovdan.ru/` opens.
|
||||
- [ ] `https://www.arhipovdan.ru/` opens.
|
||||
- [ ] `https://ai-shorts.ru/` still works and was not affected.
|
||||
- [ ] Presentation Mode opens.
|
||||
- [ ] Demo steps checked.
|
||||
- [ ] Safe Demo confirmation checked for jam/emergency.
|
||||
- [ ] OZON Criteria Coverage Panel checked.
|
||||
- [ ] Engineering Mode still works.
|
||||
- [ ] Scenarios checked: normal_flow, oversized_item, round_object, boundary_dimensions, close_items, low_confidence, jam, emergency_stop.
|
||||
- [ ] Docs present: README, ARCHITECTURE, DEMO_SCRIPT, SCENARIOS, JURY_QA, SUBMISSION_CHECKLIST.
|
||||
- [ ] Branch pushed to `origin/dan_branch`.
|
||||
- [ ] Nginx backup path from deploy is known if infrastructure rollback is needed.
|
||||
67
src/App.tsx
67
src/App.tsx
@@ -1,17 +1,27 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import { DEMO_STEPS } from './data/demoSteps';
|
||||
import { SCENARIOS } from './data/scenarios';
|
||||
import { createSimulation, setRunning, stepSimulation, stepSimulationToNextState } from './domain/simulation';
|
||||
import type { ScenarioId, SimulationState } from './domain/types';
|
||||
import type { DemoStep, PresentationMode, ScenarioId, SimulationState } from './domain/types';
|
||||
|
||||
const TICK_MS = 250;
|
||||
|
||||
function isFaultDemoStep(step: DemoStep): boolean {
|
||||
return step.scenarioId === 'jam' || step.scenarioId === 'emergency_stop';
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [activeScenarioId, setActiveScenarioId] = useState<ScenarioId>('normal_flow');
|
||||
const [presentationMode, setPresentationMode] = useState<PresentationMode>('engineering');
|
||||
const [safeDemoEnabled, setSafeDemoEnabled] = useState(true);
|
||||
const [demoStepIndex, setDemoStepIndex] = useState(0);
|
||||
const [pendingFaultStepId, setPendingFaultStepId] = useState<string | undefined>();
|
||||
const activeScenario = useMemo(
|
||||
() => SCENARIOS.find((scenario) => scenario.id === activeScenarioId) ?? SCENARIOS[0],
|
||||
[activeScenarioId],
|
||||
);
|
||||
const currentDemoStep = DEMO_STEPS[demoStepIndex];
|
||||
const [simulation, setSimulation] = useState<SimulationState>(() => createSimulation(activeScenario));
|
||||
|
||||
useEffect(() => {
|
||||
@@ -34,18 +44,71 @@ export default function App() {
|
||||
const handlePause = () => setSimulation((current) => setRunning(current, false));
|
||||
const handleReset = () => setSimulation(createSimulation(activeScenario));
|
||||
const handleStep = () => setSimulation((current) => stepSimulationToNextState(current));
|
||||
const handleScenarioChange = (scenarioId: ScenarioId) => setActiveScenarioId(scenarioId);
|
||||
const handleScenarioChange = (scenarioId: ScenarioId) => {
|
||||
setPendingFaultStepId(undefined);
|
||||
setActiveScenarioId(scenarioId);
|
||||
};
|
||||
|
||||
const handleDemoStepChange = (nextIndex: number) => {
|
||||
setPendingFaultStepId(undefined);
|
||||
setDemoStepIndex(Math.max(0, Math.min(nextIndex, DEMO_STEPS.length - 1)));
|
||||
};
|
||||
|
||||
const handleApplyDemoStepScenario = (step: DemoStep) => {
|
||||
if (presentationMode === 'presentation' && safeDemoEnabled && isFaultDemoStep(step) && pendingFaultStepId !== step.id) {
|
||||
setPendingFaultStepId(step.id);
|
||||
return;
|
||||
}
|
||||
|
||||
setPendingFaultStepId(undefined);
|
||||
setActiveScenarioId(step.scenarioId);
|
||||
};
|
||||
|
||||
const handleRunPreferredAction = (step: DemoStep) => {
|
||||
if (activeScenarioId !== step.scenarioId) {
|
||||
handleApplyDemoStepScenario(step);
|
||||
return;
|
||||
}
|
||||
|
||||
if (step.preferredAction === 'start') {
|
||||
handleStart();
|
||||
} else if (step.preferredAction === 'pause') {
|
||||
handlePause();
|
||||
} else if (step.preferredAction === 'reset') {
|
||||
handleReset();
|
||||
} else {
|
||||
handleStep();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestartDemo = () => {
|
||||
setPendingFaultStepId(undefined);
|
||||
setDemoStepIndex(0);
|
||||
setActiveScenarioId(DEMO_STEPS[0].scenarioId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dashboard
|
||||
simulation={simulation}
|
||||
scenarios={SCENARIOS}
|
||||
activeScenarioId={activeScenarioId}
|
||||
presentationMode={presentationMode}
|
||||
safeDemoEnabled={safeDemoEnabled}
|
||||
demoSteps={DEMO_STEPS}
|
||||
demoStepIndex={demoStepIndex}
|
||||
currentDemoStep={currentDemoStep}
|
||||
pendingFaultStepId={pendingFaultStepId}
|
||||
onStart={handleStart}
|
||||
onPause={handlePause}
|
||||
onReset={handleReset}
|
||||
onStep={handleStep}
|
||||
onScenarioChange={handleScenarioChange}
|
||||
onPresentationModeChange={setPresentationMode}
|
||||
onSafeDemoChange={setSafeDemoEnabled}
|
||||
onDemoStepChange={handleDemoStepChange}
|
||||
onApplyDemoStepScenario={handleApplyDemoStepScenario}
|
||||
onRunPreferredAction={handleRunPreferredAction}
|
||||
onRestartDemo={handleRestartDemo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,58 +9,118 @@ import ScenarioPanel from './ScenarioPanel';
|
||||
import SensorPanel from './SensorPanel';
|
||||
import PidPanel from './PidPanel';
|
||||
import TimelinePanel from './TimelinePanel';
|
||||
import type { Scenario, ScenarioId, SimulationState } from '../domain/types';
|
||||
import PresentationModePanel from './PresentationModePanel';
|
||||
import OzonCriteriaPanel from './OzonCriteriaPanel';
|
||||
import type { DemoFocusArea, DemoStep, PresentationMode, Scenario, ScenarioId, SimulationState } from '../domain/types';
|
||||
|
||||
interface DashboardProps {
|
||||
simulation: SimulationState;
|
||||
scenarios: Scenario[];
|
||||
activeScenarioId: ScenarioId;
|
||||
presentationMode: PresentationMode;
|
||||
safeDemoEnabled: boolean;
|
||||
demoSteps: DemoStep[];
|
||||
demoStepIndex: number;
|
||||
currentDemoStep: DemoStep;
|
||||
pendingFaultStepId?: string;
|
||||
onStart: () => void;
|
||||
onPause: () => void;
|
||||
onReset: () => void;
|
||||
onStep: () => void;
|
||||
onScenarioChange: (scenarioId: ScenarioId) => void;
|
||||
onPresentationModeChange: (mode: PresentationMode) => void;
|
||||
onSafeDemoChange: (enabled: boolean) => void;
|
||||
onDemoStepChange: (index: number) => void;
|
||||
onApplyDemoStepScenario: (step: DemoStep) => void;
|
||||
onRunPreferredAction: (step: DemoStep) => void;
|
||||
onRestartDemo: () => void;
|
||||
}
|
||||
|
||||
function focusClass(mode: PresentationMode, activeFocus: DemoFocusArea, panelFocus: DemoFocusArea | DemoFocusArea[]): string {
|
||||
if (mode !== 'presentation') {
|
||||
return '';
|
||||
}
|
||||
const focusList = Array.isArray(panelFocus) ? panelFocus : [panelFocus];
|
||||
return focusList.includes(activeFocus) || (activeFocus === 'safety' && focusList.includes('scene')) ? 'focus-highlight' : 'presentation-muted';
|
||||
}
|
||||
|
||||
export default function Dashboard({
|
||||
simulation,
|
||||
scenarios,
|
||||
activeScenarioId,
|
||||
presentationMode,
|
||||
safeDemoEnabled,
|
||||
demoSteps,
|
||||
demoStepIndex,
|
||||
currentDemoStep,
|
||||
pendingFaultStepId,
|
||||
onStart,
|
||||
onPause,
|
||||
onReset,
|
||||
onStep,
|
||||
onScenarioChange,
|
||||
onPresentationModeChange,
|
||||
onSafeDemoChange,
|
||||
onDemoStepChange,
|
||||
onApplyDemoStepScenario,
|
||||
onRunPreferredAction,
|
||||
onRestartDemo,
|
||||
}: DashboardProps) {
|
||||
const focusArea = currentDemoStep.focusArea;
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<div className={`app-shell mode-${presentationMode}`}>
|
||||
<Header
|
||||
simulation={simulation}
|
||||
presentationMode={presentationMode}
|
||||
safeDemoEnabled={safeDemoEnabled}
|
||||
onStart={onStart}
|
||||
onPause={onPause}
|
||||
onReset={onReset}
|
||||
onStep={onStep}
|
||||
onPresentationModeChange={onPresentationModeChange}
|
||||
onSafeDemoChange={onSafeDemoChange}
|
||||
/>
|
||||
{presentationMode === 'presentation' ? (
|
||||
<PresentationModePanel
|
||||
steps={demoSteps}
|
||||
currentStep={currentDemoStep}
|
||||
currentIndex={demoStepIndex}
|
||||
activeScenarioId={activeScenarioId}
|
||||
safeDemoEnabled={safeDemoEnabled}
|
||||
pendingFaultStepId={pendingFaultStepId}
|
||||
onPrevious={() => onDemoStepChange(demoStepIndex - 1)}
|
||||
onNext={() => onDemoStepChange(demoStepIndex + 1)}
|
||||
onRestart={onRestartDemo}
|
||||
onApplyScenario={() => onApplyDemoStepScenario(currentDemoStep)}
|
||||
onRunPreferredAction={() => onRunPreferredAction(currentDemoStep)}
|
||||
/>
|
||||
) : null}
|
||||
<main className="dashboard-grid">
|
||||
<section className="scene-column panel panel-scene">
|
||||
<section className={`scene-column panel panel-scene ${focusClass(presentationMode, focusArea, ['scene', 'safety'])}`}>
|
||||
<SorterScene simulation={simulation} />
|
||||
</section>
|
||||
<aside className="right-rail">
|
||||
<CurrentItemPanel simulation={simulation} />
|
||||
<ClassificationPanel simulation={simulation} />
|
||||
<StateMachinePanel currentState={simulation.machineState} />
|
||||
<SensorPanel simulation={simulation} />
|
||||
<div className={focusClass(presentationMode, focusArea, 'classification')}><CurrentItemPanel simulation={simulation} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, 'classification')}><ClassificationPanel simulation={simulation} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, 'timeline')}><StateMachinePanel currentState={simulation.machineState} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, 'scene')}><SensorPanel simulation={simulation} /></div>
|
||||
</aside>
|
||||
<section className="bottom-row">
|
||||
<MetricsPanel metrics={simulation.metrics} />
|
||||
<TimelinePanel simulation={simulation} />
|
||||
<PidPanel pid={simulation.pid} metrics={simulation.metrics} status={simulation.systemStatus} />
|
||||
<EventLog events={simulation.events} />
|
||||
<ScenarioPanel
|
||||
scenarios={scenarios}
|
||||
activeScenarioId={activeScenarioId}
|
||||
onScenarioChange={onScenarioChange}
|
||||
/>
|
||||
<div className={focusClass(presentationMode, focusArea, 'timeline')}><MetricsPanel metrics={simulation.metrics} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, 'timeline')}><TimelinePanel simulation={simulation} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, ['pid', 'safety'])}><PidPanel pid={simulation.pid} metrics={simulation.metrics} status={simulation.systemStatus} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, 'eventLog')}><EventLog events={simulation.events} /></div>
|
||||
<div className={focusClass(presentationMode, focusArea, 'criteria')}>
|
||||
<OzonCriteriaPanel currentStep={currentDemoStep} />
|
||||
</div>
|
||||
<div className="scenario-panel-wrap">
|
||||
<ScenarioPanel
|
||||
scenarios={scenarios}
|
||||
activeScenarioId={activeScenarioId}
|
||||
onScenarioChange={onScenarioChange}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import type { SimulationState } from '../domain/types';
|
||||
import type { PresentationMode, SimulationState } from '../domain/types';
|
||||
|
||||
interface HeaderProps {
|
||||
simulation: SimulationState;
|
||||
presentationMode: PresentationMode;
|
||||
safeDemoEnabled: boolean;
|
||||
onStart: () => void;
|
||||
onPause: () => void;
|
||||
onReset: () => void;
|
||||
onStep: () => void;
|
||||
onPresentationModeChange: (mode: PresentationMode) => void;
|
||||
onSafeDemoChange: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Header({ simulation, onStart, onPause, onReset, onStep }: HeaderProps) {
|
||||
export default function Header({
|
||||
simulation,
|
||||
presentationMode,
|
||||
safeDemoEnabled,
|
||||
onStart,
|
||||
onPause,
|
||||
onReset,
|
||||
onStep,
|
||||
onPresentationModeChange,
|
||||
onSafeDemoChange,
|
||||
}: HeaderProps) {
|
||||
const locked = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
||||
|
||||
return (
|
||||
@@ -18,6 +32,15 @@ export default function Header({ simulation, onStart, onPause, onReset, onStep }
|
||||
<h1>OZON Tech Sorter Simulation</h1>
|
||||
</div>
|
||||
<div className="header-status">
|
||||
<div className="mode-toggle" role="group" aria-label="Dashboard mode">
|
||||
<button className={presentationMode === 'engineering' ? 'active' : ''} onClick={() => onPresentationModeChange('engineering')}>Engineering Mode</button>
|
||||
<button className={presentationMode === 'presentation' ? 'active' : ''} onClick={() => onPresentationModeChange('presentation')}>Presentation Mode</button>
|
||||
</div>
|
||||
{presentationMode === 'presentation' ? (
|
||||
<button className={safeDemoEnabled ? 'safe-demo-toggle active' : 'safe-demo-toggle'} onClick={() => onSafeDemoChange(!safeDemoEnabled)}>
|
||||
Safe Demo: {safeDemoEnabled ? 'ON' : 'OFF'}
|
||||
</button>
|
||||
) : null}
|
||||
<div className={`status-pill status-${simulation.systemStatus.toLowerCase()}`}>
|
||||
<span />
|
||||
{simulation.systemStatus}
|
||||
|
||||
45
src/components/OzonCriteriaPanel.tsx
Normal file
45
src/components/OzonCriteriaPanel.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { OZON_CRITERIA } from '../data/criteria';
|
||||
import type { CriteriaStatus, DemoStep } from '../domain/types';
|
||||
|
||||
type CriteriaFilter = 'all' | CriteriaStatus;
|
||||
|
||||
const filters: CriteriaFilter[] = ['all', 'covered', 'partially covered', 'demo step available'];
|
||||
|
||||
export default function OzonCriteriaPanel({ currentStep }: { currentStep: DemoStep }) {
|
||||
const [filter, setFilter] = useState<CriteriaFilter>('all');
|
||||
const visibleCriteria = useMemo(
|
||||
() => OZON_CRITERIA.filter((criterion) => filter === 'all' || criterion.status === filter),
|
||||
[filter],
|
||||
);
|
||||
const related = new Set(currentStep.relatedCriteria);
|
||||
|
||||
return (
|
||||
<section className="panel criteria-panel">
|
||||
<div className="panel-heading">
|
||||
<div>
|
||||
<p className="eyebrow">OZON criteria coverage</p>
|
||||
<h2>Evidence checklist</h2>
|
||||
</div>
|
||||
<span className="criteria-count">{visibleCriteria.length}/{OZON_CRITERIA.length}</span>
|
||||
</div>
|
||||
<div className="criteria-filters">
|
||||
{filters.map((item) => (
|
||||
<button key={item} className={filter === item ? 'active' : ''} onClick={() => setFilter(item)}>{item}</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="criteria-list">
|
||||
{visibleCriteria.map((criterion) => (
|
||||
<article key={criterion.id} className={related.has(criterion.id) ? 'criterion-card related' : 'criterion-card'}>
|
||||
<div>
|
||||
<strong>{criterion.title}</strong>
|
||||
<span className={`criterion-status status-${criterion.status.replaceAll(' ', '-')}`}>{criterion.status}</span>
|
||||
</div>
|
||||
<p>{criterion.evidence}</p>
|
||||
<em>{criterion.relatedScenarioIds.join(', ')} / {criterion.relatedDoc}</em>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
80
src/components/PresentationModePanel.tsx
Normal file
80
src/components/PresentationModePanel.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import type { DemoStep, ScenarioId } from '../domain/types';
|
||||
|
||||
interface PresentationModePanelProps {
|
||||
steps: DemoStep[];
|
||||
currentStep: DemoStep;
|
||||
currentIndex: number;
|
||||
activeScenarioId: ScenarioId;
|
||||
safeDemoEnabled: boolean;
|
||||
pendingFaultStepId?: string;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
onRestart: () => void;
|
||||
onApplyScenario: () => void;
|
||||
onRunPreferredAction: () => void;
|
||||
}
|
||||
|
||||
export default function PresentationModePanel({
|
||||
steps,
|
||||
currentStep,
|
||||
currentIndex,
|
||||
activeScenarioId,
|
||||
safeDemoEnabled,
|
||||
pendingFaultStepId,
|
||||
onPrevious,
|
||||
onNext,
|
||||
onRestart,
|
||||
onApplyScenario,
|
||||
onRunPreferredAction,
|
||||
}: PresentationModePanelProps) {
|
||||
const scenarioApplied = activeScenarioId === currentStep.scenarioId;
|
||||
const pendingFaultConfirmation = pendingFaultStepId === currentStep.id;
|
||||
const isFaultStep = currentStep.scenarioId === 'jam' || currentStep.scenarioId === 'emergency_stop';
|
||||
|
||||
return (
|
||||
<section className="presentation-panel panel">
|
||||
<div className="presentation-step-index">Step {currentIndex + 1} / {steps.length}</div>
|
||||
<div className="presentation-copy">
|
||||
<p className="eyebrow">Presentation Mode</p>
|
||||
<h2>{currentStep.title}</h2>
|
||||
<p>{currentStep.explanation}</p>
|
||||
</div>
|
||||
<div className="presentation-proof">
|
||||
<div>
|
||||
<strong>What to watch</strong>
|
||||
<span>{currentStep.whatToWatch}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>What it proves for jury</strong>
|
||||
<span>{currentStep.juryValue}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Current proof</strong>
|
||||
<span>classification decision {'->'} control command {'->'} physical route {'->'} logged result</span>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote>{currentStep.presenterPhrase}</blockquote>
|
||||
<div className="presentation-meta">
|
||||
<span>Recommended scenario: <strong>{currentStep.scenarioId}</strong></span>
|
||||
<span>Action: <strong>{currentStep.preferredAction}</strong></span>
|
||||
<span>Focus: <strong>{currentStep.focusArea}</strong></span>
|
||||
<span>Safe Demo: <strong>{safeDemoEnabled ? 'ON' : 'OFF'}</strong></span>
|
||||
</div>
|
||||
<div className="criteria-tags">
|
||||
{currentStep.relatedCriteria.map((criterion) => <span key={criterion}>{criterion}</span>)}
|
||||
</div>
|
||||
{safeDemoEnabled && isFaultStep && pendingFaultConfirmation ? (
|
||||
<p className="confirm-note">Click “Confirm fault scenario” again to intentionally switch to {currentStep.scenarioId}.</p>
|
||||
) : null}
|
||||
<div className="presentation-actions">
|
||||
<button onClick={onPrevious} disabled={currentIndex === 0}>Previous step</button>
|
||||
<button onClick={onNext} disabled={currentIndex === steps.length - 1}>Next step</button>
|
||||
<button onClick={onRestart}>Restart demo</button>
|
||||
<button className={scenarioApplied ? 'active' : ''} onClick={onApplyScenario}>
|
||||
{pendingFaultConfirmation ? 'Confirm fault scenario' : scenarioApplied ? 'Scenario applied' : 'Apply scenario'}
|
||||
</button>
|
||||
<button onClick={onRunPreferredAction}>{scenarioApplied ? 'Run suggested action' : 'Apply then run'}</button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
22
src/data/criteria.test.ts
Normal file
22
src/data/criteria.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { OZON_CRITERIA } from './criteria';
|
||||
import { SCENARIOS } from './scenarios';
|
||||
|
||||
const scenarioIds = new Set(SCENARIOS.map((scenario) => scenario.id));
|
||||
|
||||
describe('OZON criteria', () => {
|
||||
it('contains evidence and valid scenario references', () => {
|
||||
expect(OZON_CRITERIA.length).toBeGreaterThan(0);
|
||||
|
||||
for (const criterion of OZON_CRITERIA) {
|
||||
expect(criterion.title).toBeTruthy();
|
||||
expect(criterion.status).toMatch(/covered|partially covered|demo step available/);
|
||||
expect(criterion.evidence).toBeTruthy();
|
||||
expect(criterion.relatedDoc).toBeTruthy();
|
||||
expect(criterion.relatedScenarioIds.length).toBeGreaterThan(0);
|
||||
for (const scenarioId of criterion.relatedScenarioIds) {
|
||||
expect(scenarioIds.has(scenarioId)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
108
src/data/criteria.ts
Normal file
108
src/data/criteria.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type { OzonCriterion } from '../domain/types';
|
||||
|
||||
export const OZON_CRITERIA: OzonCriterion[] = [
|
||||
{
|
||||
id: 'category-correctness',
|
||||
title: 'Category correctness',
|
||||
status: 'covered',
|
||||
evidence: 'Classifier tests and decision tree prove B/C/D outcomes for mock SKUs.',
|
||||
relatedScenarioIds: ['normal_flow', 'oversized_item', 'round_object'],
|
||||
relatedDoc: 'docs/SCENARIOS.md',
|
||||
},
|
||||
{
|
||||
id: 'classification-rules',
|
||||
title: 'Classification rules: dimensions, roundness, priority',
|
||||
status: 'covered',
|
||||
evidence: 'ClassificationPanel shows thresholds, actual values, PASS/FAIL and C priority.',
|
||||
relatedScenarioIds: ['oversized_item', 'round_object', 'boundary_dimensions'],
|
||||
relatedDoc: 'docs/ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
id: 'boundary-cases',
|
||||
title: 'Boundary cases',
|
||||
status: 'covered',
|
||||
evidence: 'Boundary dimensions scenario and tests cover max boundary and min-width violation.',
|
||||
relatedScenarioIds: ['boundary_dimensions'],
|
||||
relatedDoc: 'docs/SCENARIOS.md',
|
||||
},
|
||||
{
|
||||
id: 'physical-routing',
|
||||
title: 'Physical routing B/C/D',
|
||||
status: 'covered',
|
||||
evidence: 'SorterScene displays B straight route, C lower route and D upper route with commands.',
|
||||
relatedScenarioIds: ['normal_flow', 'oversized_item', 'round_object'],
|
||||
relatedDoc: 'docs/DEMO_SCRIPT.md',
|
||||
},
|
||||
{
|
||||
id: 'manipulation-logic',
|
||||
title: 'Manipulation logic',
|
||||
status: 'covered',
|
||||
evidence: 'Stop-gate and pusher states are tied to state machine routes.',
|
||||
relatedScenarioIds: ['oversized_item', 'round_object'],
|
||||
relatedDoc: 'docs/ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
id: 'geometry-variety',
|
||||
title: 'Geometry variety',
|
||||
status: 'covered',
|
||||
evidence: 'Mock items include boxes, pouf, plate, bottle, cylinder, pen and boundary box.',
|
||||
relatedScenarioIds: ['normal_flow', 'round_object', 'boundary_dimensions'],
|
||||
relatedDoc: 'docs/SCENARIOS.md',
|
||||
},
|
||||
{
|
||||
id: 'safety-emergency',
|
||||
title: 'Safety / emergency stop',
|
||||
status: 'demo step available',
|
||||
evidence: 'Emergency stop scenario enters EMERGENCY_STOP and requires Reset.',
|
||||
relatedScenarioIds: ['emergency_stop'],
|
||||
relatedDoc: 'docs/DEMO_SCRIPT.md',
|
||||
},
|
||||
{
|
||||
id: 'cycle-time-throughput',
|
||||
title: 'Cycle time and throughput',
|
||||
status: 'covered',
|
||||
evidence: 'Metrics cards show processed count, avg cycle time and throughput.',
|
||||
relatedScenarioIds: ['normal_flow', 'close_items'],
|
||||
relatedDoc: 'docs/ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
id: 'timing-synchronization',
|
||||
title: 'Timing synchronization',
|
||||
status: 'covered',
|
||||
evidence: 'CycleTimeline shows state timestamps, durations and active/done status.',
|
||||
relatedScenarioIds: ['normal_flow', 'close_items'],
|
||||
relatedDoc: 'docs/ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
id: 'fault-handling',
|
||||
title: 'Fault handling',
|
||||
status: 'demo step available',
|
||||
evidence: 'Jam scenario enters FAULT, logs fault and stops conveyor.',
|
||||
relatedScenarioIds: ['jam', 'low_confidence'],
|
||||
relatedDoc: 'docs/JURY_QA.md',
|
||||
},
|
||||
{
|
||||
id: 'cv-actuator-integration',
|
||||
title: 'CV-to-actuator integration',
|
||||
status: 'covered',
|
||||
evidence: 'Pseudo-CV packet feeds classification, which emits ROUTE_TO_* actuator commands.',
|
||||
relatedScenarioIds: ['normal_flow', 'low_confidence'],
|
||||
relatedDoc: 'docs/ARCHITECTURE.md',
|
||||
},
|
||||
{
|
||||
id: 'engineering-realism',
|
||||
title: 'Engineering realism',
|
||||
status: 'partially covered',
|
||||
evidence: 'Scaled 2D layout, sensor latency, PID imitation and event trace are present; real hardware is out of MVP scope.',
|
||||
relatedScenarioIds: ['normal_flow', 'jam', 'emergency_stop'],
|
||||
relatedDoc: 'docs/JURY_QA.md',
|
||||
},
|
||||
{
|
||||
id: 'reproducibility-docs',
|
||||
title: 'Reproducibility / docs',
|
||||
status: 'covered',
|
||||
evidence: 'README and docs describe architecture, demo script, scenarios and jury Q&A.',
|
||||
relatedScenarioIds: ['normal_flow', 'boundary_dimensions'],
|
||||
relatedDoc: 'docs/DEMO_SCRIPT.md',
|
||||
},
|
||||
];
|
||||
17
src/data/demoSteps.test.ts
Normal file
17
src/data/demoSteps.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { DEMO_STEPS } from './demoSteps';
|
||||
import { SCENARIOS } from './scenarios';
|
||||
|
||||
const scenarioIds = new Set(SCENARIOS.map((scenario) => scenario.id));
|
||||
|
||||
describe('demo steps', () => {
|
||||
it('have required narrative fields and valid scenarios', () => {
|
||||
for (const step of DEMO_STEPS) {
|
||||
expect(step.id).toBeTruthy();
|
||||
expect(step.title).toBeTruthy();
|
||||
expect(scenarioIds.has(step.scenarioId)).toBe(true);
|
||||
expect(step.relatedCriteria.length).toBeGreaterThan(0);
|
||||
expect(step.presenterPhrase).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
124
src/data/demoSteps.ts
Normal file
124
src/data/demoSteps.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { DemoStep } from '../domain/types';
|
||||
|
||||
export const DEMO_STEPS: DemoStep[] = [
|
||||
{
|
||||
id: 'system-overview',
|
||||
title: 'System overview',
|
||||
scenarioId: 'normal_flow',
|
||||
preferredAction: 'reset',
|
||||
focusArea: 'scene',
|
||||
explanation: 'The line is presented as detection -> classification -> actuator command -> physical route.',
|
||||
whatToWatch: 'Scaled SVG scene, sensor positions, stop-gate, pushers and B/C/D routing arrows.',
|
||||
juryValue: 'Shows that the project is an engineering simulation of a sorting cell, not a decorative animation.',
|
||||
relatedCriteria: ['engineering-realism', 'cv-actuator-integration', 'physical-routing'],
|
||||
presenterPhrase: 'We model the whole loop: the item is detected, classified by rules, converted to an actuator command and routed physically.',
|
||||
},
|
||||
{
|
||||
id: 'normal-item-to-b',
|
||||
title: 'Normal item to B',
|
||||
scenarioId: 'normal_flow',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'classification',
|
||||
explanation: 'A regular box passes dimensions and roundness checks, so it stays on the main sorter path.',
|
||||
whatToWatch: 'Decision tree PASS/PASS and ROUTE_TO_B command in the scene and event log.',
|
||||
juryValue: 'Proves the happy path and shows category correctness for standard goods.',
|
||||
relatedCriteria: ['category-correctness', 'classification-rules', 'physical-routing'],
|
||||
presenterPhrase: 'For a normal SKU, dimensions pass and circular section is not detected, so the gate opens to B.',
|
||||
},
|
||||
{
|
||||
id: 'oversized-to-c',
|
||||
title: 'Oversized item to C',
|
||||
scenarioId: 'oversized_item',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'classification',
|
||||
explanation: 'Oversized goods fail the first rule and are routed to roll-cage C before shape is considered.',
|
||||
whatToWatch: 'Dimensions FAIL, category C, pusher C and route arrow to the lower roll-cage.',
|
||||
juryValue: 'Shows rule priority and safe diversion for non-compliant dimensions.',
|
||||
relatedCriteria: ['classification-rules', 'boundary-cases', 'manipulation-logic'],
|
||||
presenterPhrase: 'C has priority because geometry safety is checked before shape: this item cannot continue on the main sorter.',
|
||||
},
|
||||
{
|
||||
id: 'round-object-to-d',
|
||||
title: 'Round object to D',
|
||||
scenarioId: 'round_object',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'scene',
|
||||
explanation: 'Roundness >= 0.8 after valid dimensions sends the item to D for repack/shape issue handling.',
|
||||
whatToWatch: 'Roundness FAIL for B, category D and the upper route/pusher D command.',
|
||||
juryValue: 'Shows how the circle-section rule becomes a physical route.',
|
||||
relatedCriteria: ['classification-rules', 'geometry-variety', 'physical-routing'],
|
||||
presenterPhrase: 'The dimensions fit, but roundness is high, so the system sends it to D for do-upakovka.',
|
||||
},
|
||||
{
|
||||
id: 'boundary-dimensions',
|
||||
title: 'Boundary dimensions',
|
||||
scenarioId: 'boundary_dimensions',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'classification',
|
||||
explanation: 'Items at the edge of the allowed envelope prove the rule implementation is stable.',
|
||||
whatToWatch: '450 x 320 x 320 passes, while width 9 mm fails min width and goes to C.',
|
||||
juryValue: 'Shows deterministic boundary behavior rather than visual guessing.',
|
||||
relatedCriteria: ['boundary-cases', 'category-correctness', 'reproducibility-docs'],
|
||||
presenterPhrase: 'The exact boundary is accepted, but one millimeter below minimum width is rejected.',
|
||||
},
|
||||
{
|
||||
id: 'low-confidence-fallback',
|
||||
title: 'Low confidence fallback',
|
||||
scenarioId: 'low_confidence',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'eventLog',
|
||||
explanation: 'Low pseudo-CV confidence raises a warning but does not block rule-based classification.',
|
||||
whatToWatch: 'Warning message, fallback note and final category in classification/event log.',
|
||||
juryValue: 'Shows graceful degradation when CV is uncertain.',
|
||||
relatedCriteria: ['cv-actuator-integration', 'fault-handling', 'classification-rules'],
|
||||
presenterPhrase: 'CV confidence is low, so we flag it, but dimensions and roundness still produce a deterministic decision.',
|
||||
},
|
||||
{
|
||||
id: 'close-items-queue',
|
||||
title: 'Close items queue',
|
||||
scenarioId: 'close_items',
|
||||
preferredAction: 'start',
|
||||
focusArea: 'timeline',
|
||||
explanation: 'Close spacing creates a queue warning and items are still processed one cycle at a time.',
|
||||
whatToWatch: 'Queue length, queue delay, spacing warning and sequential timeline.',
|
||||
juryValue: 'Shows robustness for a common operational issue without overcomplicated physics.',
|
||||
relatedCriteria: ['timing-synchronization', 'cycle-time-throughput', 'fault-handling'],
|
||||
presenterPhrase: 'The line detects close spacing, holds the queue and keeps the state machine deterministic.',
|
||||
},
|
||||
{
|
||||
id: 'jam-fault-handling',
|
||||
title: 'Jam / fault handling',
|
||||
scenarioId: 'jam',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'safety',
|
||||
explanation: 'A jam at the stop-gate moves the system to FAULT and stops the conveyor.',
|
||||
whatToWatch: 'FAULT state, red scene overlay, PID target 0 and Reset required.',
|
||||
juryValue: 'Shows fail-safe behavior and operator recovery path.',
|
||||
relatedCriteria: ['fault-handling', 'safety-emergency', 'pid-synchronization'],
|
||||
presenterPhrase: 'When the gate jams, the safest command is to stop motion and require reset.',
|
||||
},
|
||||
{
|
||||
id: 'emergency-stop',
|
||||
title: 'Emergency stop',
|
||||
scenarioId: 'emergency_stop',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'safety',
|
||||
explanation: 'Emergency stop interrupts the line and brings motion toward zero.',
|
||||
whatToWatch: 'EMERGENCY_STOP state, stopped conveyor and PID speed falling to zero.',
|
||||
juryValue: 'Shows explicit safety handling for abnormal operation.',
|
||||
relatedCriteria: ['safety-emergency', 'fault-handling', 'engineering-realism'],
|
||||
presenterPhrase: 'Emergency stop is not another route; it is a safety state that freezes the line until reset.',
|
||||
},
|
||||
{
|
||||
id: 'performance-sync',
|
||||
title: 'Performance and synchronization',
|
||||
scenarioId: 'normal_flow',
|
||||
preferredAction: 'start',
|
||||
focusArea: 'pid',
|
||||
explanation: 'Timeline, latency cards and PID graph show how the simulated control loop stays synchronized.',
|
||||
whatToWatch: 'CycleTimeline, CV latency, actuator latency, throughput and PID speed curve.',
|
||||
juryValue: 'Shows that the demo explains timing and throughput, not only classification.',
|
||||
relatedCriteria: ['cycle-time-throughput', 'timing-synchronization', 'engineering-realism'],
|
||||
presenterPhrase: 'Here we close the loop: decisions are timestamped, actuators have latency and conveyor control is visible.',
|
||||
},
|
||||
];
|
||||
@@ -186,3 +186,31 @@ export interface SimulationState {
|
||||
events: EventLogEntry[];
|
||||
activeRoute?: Category;
|
||||
}
|
||||
|
||||
|
||||
export type PresentationMode = 'engineering' | 'presentation';
|
||||
export type DemoPreferredAction = 'start' | 'step' | 'reset' | 'pause';
|
||||
export type DemoFocusArea = 'scene' | 'classification' | 'timeline' | 'pid' | 'eventLog' | 'criteria' | 'safety';
|
||||
export type CriteriaStatus = 'covered' | 'partially covered' | 'demo step available';
|
||||
|
||||
export interface DemoStep {
|
||||
id: string;
|
||||
title: string;
|
||||
scenarioId: ScenarioId;
|
||||
preferredAction: DemoPreferredAction;
|
||||
focusArea: DemoFocusArea;
|
||||
explanation: string;
|
||||
whatToWatch: string;
|
||||
juryValue: string;
|
||||
relatedCriteria: string[];
|
||||
presenterPhrase: string;
|
||||
}
|
||||
|
||||
export interface OzonCriterion {
|
||||
id: string;
|
||||
title: string;
|
||||
status: CriteriaStatus;
|
||||
evidence: string;
|
||||
relatedScenarioIds: ScenarioId[];
|
||||
relatedDoc: string;
|
||||
}
|
||||
|
||||
271
src/styles.css
271
src/styles.css
@@ -852,3 +852,274 @@ button:disabled {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.mode-toggle {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border: 1px solid #284762;
|
||||
border-radius: 999px;
|
||||
background: #071423;
|
||||
}
|
||||
|
||||
.mode-toggle button,
|
||||
.safe-demo-toggle {
|
||||
min-height: 30px;
|
||||
padding: 4px 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.mode-toggle button.active,
|
||||
.safe-demo-toggle.active,
|
||||
.presentation-actions button.active {
|
||||
border-color: var(--cyan);
|
||||
background: rgba(56, 189, 248, 0.14);
|
||||
color: #e8f7ff;
|
||||
}
|
||||
|
||||
.mode-presentation .dashboard-grid {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.presentation-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 90px minmax(260px, 0.9fr) minmax(420px, 1.4fr);
|
||||
gap: 14px;
|
||||
margin-top: 16px;
|
||||
padding: 16px;
|
||||
border-color: rgba(56, 189, 248, 0.55);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(56, 189, 248, 0.1), rgba(12, 23, 37, 0.92) 32%),
|
||||
var(--panel);
|
||||
}
|
||||
|
||||
.presentation-step-index {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 92px;
|
||||
border: 1px solid #2d5572;
|
||||
border-radius: 16px;
|
||||
color: var(--cyan);
|
||||
font-size: 15px;
|
||||
font-weight: 950;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.presentation-copy h2 {
|
||||
margin: 4px 0 8px;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.presentation-copy p,
|
||||
.presentation-proof span,
|
||||
.presentation-meta span,
|
||||
.presentation-panel blockquote {
|
||||
color: #c6d7e8;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.presentation-proof {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.presentation-proof div {
|
||||
padding: 10px;
|
||||
border: 1px solid #284762;
|
||||
border-radius: 13px;
|
||||
background: #081422;
|
||||
}
|
||||
|
||||
.presentation-proof strong,
|
||||
.presentation-proof span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.presentation-proof strong {
|
||||
margin-bottom: 5px;
|
||||
color: #f1f8ff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.presentation-panel blockquote {
|
||||
grid-column: 2 / -1;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border-left: 3px solid var(--cyan);
|
||||
border-radius: 10px;
|
||||
background: rgba(56, 189, 248, 0.08);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.presentation-meta,
|
||||
.criteria-tags,
|
||||
.presentation-actions {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.presentation-meta span,
|
||||
.criteria-tags span,
|
||||
.criteria-count {
|
||||
padding: 5px 9px;
|
||||
border: 1px solid #284762;
|
||||
border-radius: 999px;
|
||||
background: #071423;
|
||||
color: #b9cce0;
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.criteria-tags span {
|
||||
border-color: rgba(74, 222, 128, 0.38);
|
||||
color: #bbf7d0;
|
||||
}
|
||||
|
||||
.confirm-note {
|
||||
grid-column: 1 / -1;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(245, 158, 11, 0.65);
|
||||
border-radius: 12px;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
color: #fde68a;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.presentation-actions button {
|
||||
min-height: 34px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.focus-highlight > .panel,
|
||||
.focus-highlight.panel {
|
||||
border-color: var(--cyan);
|
||||
box-shadow: 0 0 0 1px rgba(56, 189, 248, 0.3), 0 0 30px rgba(56, 189, 248, 0.12);
|
||||
}
|
||||
|
||||
.focus-highlight > .panel::before,
|
||||
.focus-highlight.panel::before {
|
||||
content: "Current demo focus";
|
||||
display: inline-flex;
|
||||
margin: -2px 0 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(56, 189, 248, 0.16);
|
||||
color: var(--cyan);
|
||||
font-size: 10px;
|
||||
font-weight: 950;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mode-presentation .presentation-muted > .panel,
|
||||
.mode-presentation .presentation-muted.panel {
|
||||
opacity: 0.74;
|
||||
}
|
||||
|
||||
.criteria-panel {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.criteria-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.criteria-filters button {
|
||||
min-height: 28px;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.criteria-filters button.active {
|
||||
border-color: var(--cyan);
|
||||
background: rgba(56, 189, 248, 0.12);
|
||||
}
|
||||
|
||||
.criteria-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
max-height: 290px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.criterion-card {
|
||||
padding: 10px;
|
||||
border: 1px solid #243e57;
|
||||
border-radius: 13px;
|
||||
background: #081422;
|
||||
}
|
||||
|
||||
.criterion-card.related {
|
||||
border-color: var(--green);
|
||||
background: rgba(74, 222, 128, 0.07);
|
||||
}
|
||||
|
||||
.criterion-card div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.criterion-card strong,
|
||||
.criterion-card p,
|
||||
.criterion-card em {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.criterion-card p {
|
||||
color: #c1d1e3;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.criterion-card em {
|
||||
margin-top: 6px;
|
||||
color: var(--muted);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.criterion-status {
|
||||
white-space: nowrap;
|
||||
color: var(--cyan);
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-covered { color: var(--green); }
|
||||
.status-partially-covered { color: var(--orange); }
|
||||
.status-demo-step-available { color: var(--purple); }
|
||||
|
||||
.scenario-panel-wrap {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 1500px) {
|
||||
.presentation-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.presentation-panel blockquote,
|
||||
.presentation-meta,
|
||||
.criteria-tags,
|
||||
.presentation-actions,
|
||||
.confirm-note {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.presentation-proof {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user