diff --git a/README.md b/README.md index a19f2fa..b456609 100644 --- a/README.md +++ b/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 diff --git a/docs/DEMO_SCRIPT.md b/docs/DEMO_SCRIPT.md index dc56fc6..5f54139 100644 --- a/docs/DEMO_SCRIPT.md +++ b/docs/DEMO_SCRIPT.md @@ -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. diff --git a/docs/JURY_QA.md b/docs/JURY_QA.md index 749f0de..aa8d304 100644 --- a/docs/JURY_QA.md +++ b/docs/JURY_QA.md @@ -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. diff --git a/docs/SUBMISSION_CHECKLIST.md b/docs/SUBMISSION_CHECKLIST.md new file mode 100644 index 0000000..8fe232c --- /dev/null +++ b/docs/SUBMISSION_CHECKLIST.md @@ -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. diff --git a/src/App.tsx b/src/App.tsx index a1c8932..4f9d6d7 100644 --- a/src/App.tsx +++ b/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('normal_flow'); + const [presentationMode, setPresentationMode] = useState('engineering'); + const [safeDemoEnabled, setSafeDemoEnabled] = useState(true); + const [demoStepIndex, setDemoStepIndex] = useState(0); + const [pendingFaultStepId, setPendingFaultStepId] = useState(); const activeScenario = useMemo( () => SCENARIOS.find((scenario) => scenario.id === activeScenarioId) ?? SCENARIOS[0], [activeScenarioId], ); + const currentDemoStep = DEMO_STEPS[demoStepIndex]; const [simulation, setSimulation] = useState(() => 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 ( ); } diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 1b7da69..87f4219 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -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 ( -
+
+ {presentationMode === 'presentation' ? ( + onDemoStepChange(demoStepIndex - 1)} + onNext={() => onDemoStepChange(demoStepIndex + 1)} + onRestart={onRestartDemo} + onApplyScenario={() => onApplyDemoStepScenario(currentDemoStep)} + onRunPreferredAction={() => onRunPreferredAction(currentDemoStep)} + /> + ) : null}
-
+
- - - - - +
+
+
+
+
+ +
+
+ +
diff --git a/src/components/Header.tsx b/src/components/Header.tsx index d694eb9..f62af76 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -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 }

OZON Tech Sorter Simulation

+
+ + +
+ {presentationMode === 'presentation' ? ( + + ) : null}
{simulation.systemStatus} diff --git a/src/components/OzonCriteriaPanel.tsx b/src/components/OzonCriteriaPanel.tsx new file mode 100644 index 0000000..7f754a0 --- /dev/null +++ b/src/components/OzonCriteriaPanel.tsx @@ -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('all'); + const visibleCriteria = useMemo( + () => OZON_CRITERIA.filter((criterion) => filter === 'all' || criterion.status === filter), + [filter], + ); + const related = new Set(currentStep.relatedCriteria); + + return ( +
+
+
+

OZON criteria coverage

+

Evidence checklist

+
+ {visibleCriteria.length}/{OZON_CRITERIA.length} +
+
+ {filters.map((item) => ( + + ))} +
+
+ {visibleCriteria.map((criterion) => ( +
+
+ {criterion.title} + {criterion.status} +
+

{criterion.evidence}

+ {criterion.relatedScenarioIds.join(', ')} / {criterion.relatedDoc} +
+ ))} +
+
+ ); +} diff --git a/src/components/PresentationModePanel.tsx b/src/components/PresentationModePanel.tsx new file mode 100644 index 0000000..586e38d --- /dev/null +++ b/src/components/PresentationModePanel.tsx @@ -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 ( +
+
Step {currentIndex + 1} / {steps.length}
+
+

Presentation Mode

+

{currentStep.title}

+

{currentStep.explanation}

+
+
+
+ What to watch + {currentStep.whatToWatch} +
+
+ What it proves for jury + {currentStep.juryValue} +
+
+ Current proof + classification decision {'->'} control command {'->'} physical route {'->'} logged result +
+
+
{currentStep.presenterPhrase}
+
+ Recommended scenario: {currentStep.scenarioId} + Action: {currentStep.preferredAction} + Focus: {currentStep.focusArea} + Safe Demo: {safeDemoEnabled ? 'ON' : 'OFF'} +
+
+ {currentStep.relatedCriteria.map((criterion) => {criterion})} +
+ {safeDemoEnabled && isFaultStep && pendingFaultConfirmation ? ( +

Click “Confirm fault scenario” again to intentionally switch to {currentStep.scenarioId}.

+ ) : null} +
+ + + + + +
+
+ ); +} diff --git a/src/data/criteria.test.ts b/src/data/criteria.test.ts new file mode 100644 index 0000000..e44e91f --- /dev/null +++ b/src/data/criteria.test.ts @@ -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); + } + } + }); +}); diff --git a/src/data/criteria.ts b/src/data/criteria.ts new file mode 100644 index 0000000..af7499b --- /dev/null +++ b/src/data/criteria.ts @@ -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', + }, +]; diff --git a/src/data/demoSteps.test.ts b/src/data/demoSteps.test.ts new file mode 100644 index 0000000..116c1dd --- /dev/null +++ b/src/data/demoSteps.test.ts @@ -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(); + } + }); +}); diff --git a/src/data/demoSteps.ts b/src/data/demoSteps.ts new file mode 100644 index 0000000..5bb1481 --- /dev/null +++ b/src/data/demoSteps.ts @@ -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.', + }, +]; diff --git a/src/domain/types.ts b/src/domain/types.ts index 9dadfd0..0ec1875 100644 --- a/src/domain/types.ts +++ b/src/domain/types.ts @@ -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; +} diff --git a/src/styles.css b/src/styles.css index 41a0fd2..c72d7ab 100644 --- a/src/styles.css +++ b/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; + } +}