design: redesign product demo UI
This commit is contained in:
22
.cursor/rules/accessibility-and-visual-qa.mdc
Normal file
22
.cursor/rules/accessibility-and-visual-qa.mdc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
description: Accessibility and visual QA checklist for product demo
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Accessibility and Visual QA Rules
|
||||||
|
|
||||||
|
- Контраст должен быть читаемым.
|
||||||
|
- Фокус-состояния для кнопок обязательны.
|
||||||
|
- Кнопки должны иметь понятные labels.
|
||||||
|
- Не использовать только цвет для передачи смысла.
|
||||||
|
- Текст должен быть читаемым на проекторе.
|
||||||
|
- Проверить визуально:
|
||||||
|
- 1920×1080
|
||||||
|
- 1440×900
|
||||||
|
- 390×844
|
||||||
|
- Проверить:
|
||||||
|
- нет horizontal scroll;
|
||||||
|
- hero понятен;
|
||||||
|
- CTA виден;
|
||||||
|
- demo section понятен;
|
||||||
|
- mobile layout не развален.
|
||||||
24
.cursor/rules/react-design-system.mdc
Normal file
24
.cursor/rules/react-design-system.mdc
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
description: React design system and component boundaries for sorter demo
|
||||||
|
globs: src/**/*.{tsx,ts,css}
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
# React Design System Rules
|
||||||
|
|
||||||
|
- Не ломать domain logic.
|
||||||
|
- UI-компоненты должны быть небольшими и понятными.
|
||||||
|
- Переиспользовать данные scenarios / criteria / demoSteps.
|
||||||
|
- Не добавлять тяжёлые UI-библиотеки.
|
||||||
|
- Не добавлять 3D.
|
||||||
|
- Не добавлять backend.
|
||||||
|
- Компоненты:
|
||||||
|
- HeroSection
|
||||||
|
- ProductDemoSection
|
||||||
|
- StorylineStepper
|
||||||
|
- ScenarioCards
|
||||||
|
- CriteriaCards
|
||||||
|
- EngineeringDetails
|
||||||
|
- Старые инженерные панели можно использовать внутри EngineeringDetails.
|
||||||
|
- Стили централизовать в styles.css или небольших CSS-блоках.
|
||||||
|
- Не делать хаотичный refactor всего проекта.
|
||||||
28
.cursor/rules/responsive-product-demo.mdc
Normal file
28
.cursor/rules/responsive-product-demo.mdc
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
description: Responsive breakpoints and layout rules for product demo
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Responsive Product Demo Rules
|
||||||
|
|
||||||
|
Breakpoints:
|
||||||
|
- desktop: >= 1200px
|
||||||
|
- laptop/tablet landscape: 900–1199px
|
||||||
|
- tablet: 640–899px
|
||||||
|
- mobile: < 640px
|
||||||
|
|
||||||
|
Требования:
|
||||||
|
- body/html/#root не должны создавать horizontal scroll.
|
||||||
|
- Проверять overflow-x.
|
||||||
|
- Все секции используют width: min(100%, ...), max-width, padding через clamp().
|
||||||
|
- На mobile:
|
||||||
|
- одна колонка;
|
||||||
|
- крупные кнопки;
|
||||||
|
- короткие тексты;
|
||||||
|
- инженерные детали свернуты;
|
||||||
|
- сцена масштабируется;
|
||||||
|
- сценарии идут карточками.
|
||||||
|
- На desktop:
|
||||||
|
- Hero + demo в понятных секциях;
|
||||||
|
- 2 колонки только там, где хватает ширины;
|
||||||
|
- без внутреннего хаотичного скролла.
|
||||||
33
.cursor/rules/ui-ux-pro-max.mdc
Normal file
33
.cursor/rules/ui-ux-pro-max.mdc
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
description: UI/UX Pro Max — product demo page, not debug dashboard
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI/UX Pro Max Rules
|
||||||
|
|
||||||
|
Ты обязан проектировать интерфейс как demo product page, а не debug dashboard.
|
||||||
|
|
||||||
|
Обязательные принципы:
|
||||||
|
- Пользователь должен понять смысл проекта за 10 секунд.
|
||||||
|
- Первый экран должен объяснять:
|
||||||
|
1. Что это за система.
|
||||||
|
2. Что она делает.
|
||||||
|
3. Как запустить демо.
|
||||||
|
4. Какой результат будет показан.
|
||||||
|
- Главный интерфейс должен вести пользователя по цепочке:
|
||||||
|
Detection → Classification → Command → Routing.
|
||||||
|
- Не показывать все инженерные панели сразу.
|
||||||
|
- Инженерные детали должны быть ниже или в отдельном раскрываемом блоке.
|
||||||
|
- Не должно быть горизонтального скролла.
|
||||||
|
- Mobile-first адаптив обязателен.
|
||||||
|
- Основной текст не меньше 16px.
|
||||||
|
- Кнопки не меньше 44px по высоте.
|
||||||
|
- На mobile всё в одну колонку.
|
||||||
|
- На desktop допустима сетка 2 колонки.
|
||||||
|
- Визуальная иерархия:
|
||||||
|
Hero → Demo → Result → Scenarios → Criteria → Engineering Details.
|
||||||
|
- Запрещены мелкие debug-панели на первом экране.
|
||||||
|
- Запрещены узкие внутренние scroll-панели для сценариев.
|
||||||
|
- Запрещены fixed width больше viewport.
|
||||||
|
- Использовать min-width: 0 для grid/flex children.
|
||||||
|
- SVG/сцена должны масштабироваться через width: 100%, max-width: 100%.
|
||||||
73
README.md
73
README.md
@@ -8,12 +8,12 @@
|
|||||||
- маршрутизация в зоны B/C/D;
|
- маршрутизация в зоны B/C/D;
|
||||||
- отработка аварийных ситуаций (Jam, Emergency Stop).
|
- отработка аварийных ситуаций (Jam, Emergency Stop).
|
||||||
|
|
||||||
UI переработан под **Product Demo Landing Page**, который:
|
UI — **Product Demo Landing Page**:
|
||||||
- работает без горизонтального скролла на любых экранах;
|
- без горизонтального скролла на desktop / laptop / mobile;
|
||||||
- адаптирован для Mobile, Tablet и Desktop;
|
- mobile-first адаптив (одна колонка < 640px);
|
||||||
- включает Storyline (Detection → Classification → Command → Routing);
|
- storyline Detection → Classification → Decision → Command → Routing;
|
||||||
- предлагает список Jury Scenarios и OZON Criteria;
|
- карточки сценариев и критериев OZON;
|
||||||
- прячет сложный Engineering Dashboard в сворачиваемый аккордеон.
|
- Engineering Details свёрнуты по умолчанию.
|
||||||
|
|
||||||
## Стек
|
## Стек
|
||||||
|
|
||||||
@@ -35,13 +35,46 @@ https://www.arhipovdan.ru/
|
|||||||
http://127.0.0.1:3100/
|
http://127.0.0.1:3100/
|
||||||
```
|
```
|
||||||
|
|
||||||
## Структура Demo-страницы
|
## Как устроен новый UI
|
||||||
|
|
||||||
1. **Hero Section:** краткая суть проекта и цепочки действий.
|
1. **Hero** — что это за система, CTA «Запустить демо», цепочка Detection → Routing.
|
||||||
2. **Product Demo Section:** живая сцена (`SorterScene`) и карточка текущего результата.
|
2. **Product Demo** — упрощённая сцена (`SorterScene variant="simple"`) + карточка результата + Start / Next / Reset.
|
||||||
3. **Scenarios Cards:** карточки для проверки нестандартных товаров (негабарит, шар, затор).
|
3. **Storyline Stepper** — текущий этап цикла.
|
||||||
4. **Criteria Cards:** чек-лист покрытия требований OZON.
|
4. **Scenario Cards** — jury-кейсы карточками (кнопка «Показать»).
|
||||||
5. **Engineering Details:** подробные отладочные панели (Event Log, PID, метрики).
|
5. **Criteria Cards** — покрытие критериев OZON со ссылкой на сценарий.
|
||||||
|
6. **Engineering Details** — полные техпанели (state machine, sensors, PID, timeline, event log, criteria).
|
||||||
|
|
||||||
|
## Как запустить демо
|
||||||
|
|
||||||
|
1. Откройте сайт.
|
||||||
|
2. Нажмите **Запустить демо** в Hero или **Start demo** в блоке демо.
|
||||||
|
3. Нажимайте **Next step**, чтобы пройти цикл товара.
|
||||||
|
4. Выберите сценарий в карточках ниже (негабарит, круглый объект, jam и т.д.).
|
||||||
|
5. Для экспертов откройте **Инженерный режим** / **Engineering Details**.
|
||||||
|
|
||||||
|
## Где Engineering Details
|
||||||
|
|
||||||
|
Внизу страницы, секция **Engineering Details**. По умолчанию свёрнута. Кнопки «Инженерный режим» в header / hero / demo раскрывают блок и скроллят к нему. Внутри — полная сцена (`variant="full"`) и все инженерные панели.
|
||||||
|
|
||||||
|
## Как проверить mobile
|
||||||
|
|
||||||
|
1. Откройте DevTools → device toolbar.
|
||||||
|
2. Выберите `390×844` (или iPhone 12/13).
|
||||||
|
3. Проверьте:
|
||||||
|
- одна колонка;
|
||||||
|
- кнопки ≥ 44px;
|
||||||
|
- сцена масштабируется (`width: 100%`);
|
||||||
|
- нет horizontal scroll.
|
||||||
|
|
||||||
|
## Как проверить отсутствие horizontal scroll
|
||||||
|
|
||||||
|
В консоли браузера:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.documentElement.scrollWidth <= document.documentElement.clientWidth
|
||||||
|
```
|
||||||
|
|
||||||
|
Должно вернуть `true` на ширинах 1920, 1440 и 390.
|
||||||
|
|
||||||
## Запуск локально
|
## Запуск локально
|
||||||
|
|
||||||
@@ -116,7 +149,7 @@ State machine управляет циклом:
|
|||||||
|
|
||||||
## Simplified PID
|
## Simplified PID
|
||||||
|
|
||||||
PID-панель показывает упрощенную имитацию control loop: target speed, actual speed, error, correction и mini graph последних тиков скорости.
|
PID-панель (в Engineering Details) показывает упрощенную имитацию control loop: target speed, actual speed, error, correction и mini graph последних тиков скорости.
|
||||||
|
|
||||||
В normal flow actual speed приближается к target. В `jam` и `emergency_stop` target становится 0, actual speed визуально падает к 0.
|
В normal flow actual speed приближается к target. В `jam` и `emergency_stop` target становится 0, actual speed визуально падает к 0.
|
||||||
|
|
||||||
@@ -131,11 +164,21 @@ npm run test
|
|||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
- `docs/ARCHITECTURE.md` — модули и поток данных.
|
- `docs/ARCHITECTURE.md` — модули и поток данных.
|
||||||
- `docs/DEMO_SCRIPT.md` — 3-5 минутный сценарий защиты.
|
- `docs/DEMO_SCRIPT.md` — сценарий защиты.
|
||||||
- `docs/SCENARIOS.md` — ожидаемые результаты сценариев.
|
- `docs/SCENARIOS.md` — ожидаемые результаты сценариев.
|
||||||
- `docs/JURY_QA.md` — короткие ответы на вопросы жюри.
|
- `docs/JURY_QA.md` — ответы на вопросы жюри.
|
||||||
|
- `docs/UI_UX_REDESIGN_AUDIT.md` — план редизайна UI.
|
||||||
- `docs/SUBMISSION_CHECKLIST.md` — checklist перед сдачей.
|
- `docs/SUBMISSION_CHECKLIST.md` — checklist перед сдачей.
|
||||||
|
|
||||||
|
## Cursor rules
|
||||||
|
|
||||||
|
Локальные UI/UX rules в `.cursor/rules/`:
|
||||||
|
|
||||||
|
- `ui-ux-pro-max.mdc`
|
||||||
|
- `responsive-product-demo.mdc`
|
||||||
|
- `react-design-system.mdc`
|
||||||
|
- `accessibility-and-visual-qa.mdc`
|
||||||
|
|
||||||
## Ограничения MVP
|
## Ограничения MVP
|
||||||
|
|
||||||
- Физика движения дискретная, без динамической модели массы/трения.
|
- Физика движения дискретная, без динамической модели массы/трения.
|
||||||
|
|||||||
@@ -1,79 +1,97 @@
|
|||||||
# Demo Script
|
# Demo Script
|
||||||
|
|
||||||
Open https://arhipovdan.ru/ and switch Header to `Guided Demo`.
|
Open https://arhipovdan.ru/ (или http://127.0.0.1:3100/).
|
||||||
|
|
||||||
|
Страница — product demo: Hero → Demo → Storyline → Scenarios → Criteria → Engineering Details.
|
||||||
|
|
||||||
## 30-Second Pitch
|
## 30-Second Pitch
|
||||||
|
|
||||||
"We built an engineering simulation of the OZON sorting cell. It models the full loop: detection -> classification -> command -> routing. We cover happy paths to B/C/D, fault handling, and timing. The Guided Demo view shows the live proof."
|
«Мы сделали инженерную симуляцию сортировочной ячейки OZON. Полный цикл: detection → classification → command → routing. Показываем happy path B/C/D, fault handling и timing. Главный экран — product demo, инженерные панели ниже.»
|
||||||
|
|
||||||
## 3-Minute Guided Demo
|
## 3-Minute Demo
|
||||||
|
|
||||||
1. Open **Guided Demo**.
|
1. На первом экране укажите Hero: что это за система и цепочку Detection → Classification → Command → Routing.
|
||||||
2. Click **Start Guided Demo**.
|
2. Нажмите **Запустить демо**.
|
||||||
3. Point to the **Current Proof Card** and the **Scene**.
|
3. В блоке **Главное демо** покажите упрощённую сцену и карточку результата:
|
||||||
- "Watch how detection leads to classification and routing."
|
- товар;
|
||||||
4. Click **Next** to show `Oversized item to C`.
|
- категория B/C/D;
|
||||||
- "Dimensions fail first, priority C."
|
- причина решения;
|
||||||
5. Click **Next** to show `Round object to D`.
|
- команда `ROUTE_TO_*`;
|
||||||
- "Dimensions pass, but roundness triggers D."
|
- целевая зона.
|
||||||
6. Click **Next** through `Boundary`, `Low confidence`, `Jam`, `Emergency stop`.
|
4. Нажимайте **Next step**, следите за **Этапами цикла**.
|
||||||
- On faults: click "Apply fault scenario" to confirm.
|
5. В **Сценариях** нажмите **Показать** на «Негабарит» — приоритет C.
|
||||||
- "The line stops safely on emergency."
|
6. Затем «Круглый объект» — зона D.
|
||||||
|
7. Затем «Застревание» или «Аварийная остановка» — FAULT / EMERGENCY_STOP, Reset.
|
||||||
|
|
||||||
## 5-Minute Guided Demo
|
## 5-Minute Demo
|
||||||
|
|
||||||
Use all 10 demo steps in Guided Demo View:
|
Пройдите сценарии по карточкам:
|
||||||
|
|
||||||
1. System overview.
|
1. Обычный товар — B/C/D поток.
|
||||||
2. Normal item to B.
|
2. Негабарит — C.
|
||||||
3. Oversized item to C.
|
3. Круглый объект — D.
|
||||||
4. Round object to D.
|
4. Пограничные размеры — строгие min/max.
|
||||||
5. Boundary dimensions.
|
5. Низкая уверенность CV — fallback.
|
||||||
6. Low confidence fallback.
|
6. Очередь товаров — queue/spacing.
|
||||||
7. Close items queue.
|
7. Застревание — FAULT.
|
||||||
8. Jam / fault handling.
|
8. Аварийная остановка — EMERGENCY_STOP.
|
||||||
9. Emergency stop.
|
|
||||||
10. Performance and synchronization.
|
|
||||||
|
|
||||||
For each step:
|
Для каждого:
|
||||||
|
|
||||||
1. Read the **Demo Narration Card**.
|
1. Нажмите **Показать**.
|
||||||
2. Use **Next** or the **Primary Action Button**.
|
2. **Start demo** / **Next step**.
|
||||||
3. Point to the highlighted values in the **Proof Card**.
|
3. Укажите категорию, причину и маршрут на карточке результата.
|
||||||
4. Read the **Presenter Phrase** from the screen.
|
4. Покажите активный шаг в Storyline Stepper.
|
||||||
|
|
||||||
## What to say on each step
|
## Engineering Details
|
||||||
|
|
||||||
Use the `presenterPhrase` text shown in the **Demo Narration Card**. It is specifically written to be short and clear for the jury.
|
Если жюри просит PID, event log, sensors или полный layout:
|
||||||
|
|
||||||
## What to do if demo gets stuck
|
1. Нажмите **Инженерный режим** (header / hero / demo).
|
||||||
|
2. Откроется секция **Engineering Details** с полными панелями.
|
||||||
|
3. Полная сцена — `SorterScene variant="full"`.
|
||||||
|
|
||||||
- Click **Reset Demo**.
|
## Что делать, если демо зависло
|
||||||
- If still stuck, switch back to **Engineering Mode** and use manual scenario selection.
|
|
||||||
- Proceed manually.
|
|
||||||
|
|
||||||
## Fallback If Domain Does Not Open
|
1. **Reset** в header или в блоке демо.
|
||||||
|
2. Выберите сценарий заново карточкой **Показать**.
|
||||||
|
3. При необходимости откройте Engineering Details и смените сценарий там.
|
||||||
|
|
||||||
1. SSH to server.
|
## Fallback, если домен не открывается
|
||||||
2. Check local frontend:
|
|
||||||
|
1. SSH на сервер.
|
||||||
|
2. Проверьте локальный frontend:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -I http://127.0.0.1:3100/
|
curl -I http://127.0.0.1:3100/
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Check Docker project:
|
3. Проверьте Docker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -p owl -f /opt/arhipovdan/app/docker-compose.server.yml ps
|
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.
|
## Mobile / no horizontal scroll
|
||||||
|
|
||||||
## What Each Screen Proves
|
Перед защитой проверьте:
|
||||||
|
|
||||||
- SVG scene proves physical route and actuator command.
|
- 1920×1080 — hero + demo читаемы, CTA видны;
|
||||||
- Classification panel proves why category B/C/D was selected.
|
- 1440×900 — нет debug-dashboard на первом экране;
|
||||||
- Timeline proves synchronization and step-by-step behavior.
|
- 390×844 — одна колонка, крупные кнопки, сцена не вылезает.
|
||||||
- PID panel proves conveyor control-loop behavior.
|
|
||||||
- Event Log proves traceability.
|
В консоли:
|
||||||
- Criteria panel proves coverage of OZON evaluation points.
|
|
||||||
|
```js
|
||||||
|
document.documentElement.scrollWidth <= document.documentElement.clientWidth
|
||||||
|
```
|
||||||
|
|
||||||
|
## What Each Block Proves
|
||||||
|
|
||||||
|
- Hero — смысл проекта за 10 секунд.
|
||||||
|
- Product Demo scene — физический маршрут и команда.
|
||||||
|
- Proof card — почему выбрана категория B/C/D.
|
||||||
|
- Storyline — текущий этап цикла.
|
||||||
|
- Scenario cards — jury test cases без узкого скролл-списка.
|
||||||
|
- Criteria cards — покрытие критериев OZON.
|
||||||
|
- Engineering Details — timeline, PID, event log, sensors.
|
||||||
|
|||||||
@@ -10,23 +10,23 @@ The animation is driven by a deterministic state machine, classifier, sensor sta
|
|||||||
|
|
||||||
## 3. How Do You Prove The Route Was Correct?
|
## 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.
|
The proof card shows category, reason and `ROUTE_TO_*`. The scene highlights the selected route arrow and zone B/C/D. Event Log in Engineering Details records state, command and item id.
|
||||||
|
|
||||||
## 4. Where Are OZON Rules Reflected?
|
## 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.
|
The rules are explicit in classification limits: min/max dimensions, roundness threshold and C priority. Criteria Cards map these rules to scenarios. Full checklist is in Engineering Details.
|
||||||
|
|
||||||
## 5. How Does The Operator Safely Stop The System?
|
## 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.
|
Open scenario **Аварийная остановка**. The system enters `EMERGENCY_STOP`, stops conveyor target speed and requires Reset.
|
||||||
|
|
||||||
## 6. What Happens On Sensor Failure Or Low Confidence?
|
## 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.
|
Low confidence is logged as a warning. The system still applies deterministic dimensions/roundness rules as a fallback and shows this on the proof card.
|
||||||
|
|
||||||
## 7. Why 2D, Not 3D?
|
## 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.
|
2D is enough for MVP validation: it shows geometry, timing, sensor positions, routes and state transitions without heavy rendering.
|
||||||
|
|
||||||
## 8. What Is Needed For A Physical Prototype?
|
## 8. What Is Needed For A Physical Prototype?
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ The MVP uses pseudo-CV: bbox, dimensions, confidence and latency are derived fro
|
|||||||
|
|
||||||
## 10. 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.
|
The proof card shows dimensions, roundness and reason. Tests cover key boundary cases. Engineering Details has the full decision evidence and criteria checklist.
|
||||||
|
|
||||||
## 11. How Are Dimensions And Circular Section Handled?
|
## 11. How Are Dimensions And Circular Section Handled?
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ Oversized or undersized items are operationally unsafe for the main line and mus
|
|||||||
|
|
||||||
## 13. How Is Synchronization Shown?
|
## 13. How Is Synchronization Shown?
|
||||||
|
|
||||||
Cycle timeline shows state order, simulated timestamps, durations and status: done, active, pending or skipped.
|
Storyline Stepper shows the active stage. In Engineering Details, Cycle Timeline shows state order, timestamps, durations and status: done, active, pending or skipped.
|
||||||
|
|
||||||
## 14. How Does The Actuator Part Work?
|
## 14. How Does The Actuator Part Work?
|
||||||
|
|
||||||
@@ -58,16 +58,24 @@ The stop-gate fixes the item. B opens the gate, C extends pusher C, D extends pu
|
|||||||
|
|
||||||
## 15. 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.
|
Open scenario **Застревание**. The system enters FAULT, conveyor target speed becomes 0, actual speed decays toward 0, and Reset is required.
|
||||||
|
|
||||||
## 16. Why is the interface a Product Demo Page now?
|
## 16. Why is the interface a Product Demo Page now?
|
||||||
|
|
||||||
To avoid cognitive overload during the pitch. The jury needs to understand the decision and see the route clearly without distraction from raw JSON logs or PID graphs, especially on laptop projectors or mobile devices.
|
To avoid cognitive overload during the pitch. The jury needs to understand the decision and see the route clearly without distraction from raw logs or PID graphs on the first screen.
|
||||||
|
|
||||||
## 17. How does the Demo prove engineering realism?
|
## 17. How does the Demo prove engineering realism?
|
||||||
|
|
||||||
Even in the Product Demo Section, the scene and proof card are driven by the real underlying state machine, classifier, and metric engine—not a pre-rendered video.
|
Even in the Product Demo Section, the scene and proof card are driven by the real state machine, classifier and metrics — not a pre-rendered video.
|
||||||
|
|
||||||
## 18. Where to see the full event log and PID?
|
## 18. Where to see the full event log and PID?
|
||||||
|
|
||||||
Scroll down and open the "Engineering Details (Engineering Mode)" accordion.
|
Scroll to **Engineering Details** or click **Инженерный режим**. Open the section to see state machine, sensors, PID, timeline, event log and full criteria checklist.
|
||||||
|
|
||||||
|
## 19. How to check mobile and no horizontal scroll?
|
||||||
|
|
||||||
|
Use widths 1920×1080, 1440×900 and 390×844. On mobile the layout is one column, buttons are large, the scene scales with `width: 100%`. In console:
|
||||||
|
|
||||||
|
```js
|
||||||
|
document.documentElement.scrollWidth <= document.documentElement.clientWidth
|
||||||
|
```
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ Before final submission:
|
|||||||
- [ ] `https://arhipovdan.ru/` opens.
|
- [ ] `https://arhipovdan.ru/` opens.
|
||||||
- [ ] `https://www.arhipovdan.ru/` opens.
|
- [ ] `https://www.arhipovdan.ru/` opens.
|
||||||
- [ ] `https://ai-shorts.ru/` still works and was not affected.
|
- [ ] `https://ai-shorts.ru/` still works and was not affected.
|
||||||
- [ ] Presentation Mode opens.
|
- [ ] Product Demo page opens (Hero first, not engineering dashboard).
|
||||||
- [ ] Demo steps checked.
|
- [ ] Start demo / Next step / Reset work.
|
||||||
- [ ] Safe Demo confirmation checked for jam/emergency.
|
- [ ] Storyline Stepper updates with machine state.
|
||||||
- [ ] App is responsive on Desktop (1920x1080), Laptop (1440x900) and Mobile (390x844).
|
- [ ] Scenario cards open cases with **Показать**.
|
||||||
- [ ] NO horizontal scroll on body/app container.
|
- [ ] Criteria cards show OZON coverage and linked scenarios.
|
||||||
- [ ] Product Demo layout shows step, scene, and result.
|
- [ ] Engineering Details is collapsed by default and opens from CTA.
|
||||||
- [ ] Scenarios are displayed as cards.
|
- [ ] Engineering Details shows full panels (state machine, sensors, PID, timeline, event log, criteria).
|
||||||
- [ ] Engineering Details accordion works and shows all technical panels.
|
- [ ] App is responsive on Desktop (1920×1080), Laptop (1440×900) and Mobile (390×844).
|
||||||
- [ ] Engineering Mode still works.
|
- [ ] NO horizontal scroll on body / `#root` / product page.
|
||||||
|
- [ ] Mobile: one column, buttons ≥ 44px, scene scales.
|
||||||
- [ ] Scenarios checked: normal_flow, oversized_item, round_object, boundary_dimensions, close_items, low_confidence, jam, emergency_stop.
|
- [ ] 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.
|
- [ ] Docs present: README, ARCHITECTURE, DEMO_SCRIPT, SCENARIOS, JURY_QA, UI_UX_REDESIGN_AUDIT, SUBMISSION_CHECKLIST.
|
||||||
- [ ] Branch pushed to `origin/dan_branch`.
|
- [ ] Cursor rules present in `.cursor/rules/`.
|
||||||
- [ ] Nginx backup path from deploy is known if infrastructure rollback is needed.
|
- [ ] Branch ready for manual push to `origin/dan_branch`.
|
||||||
|
- [ ] Nginx / SSL / neighboring projects were not modified.
|
||||||
|
|||||||
58
docs/UI_UX_REDESIGN_AUDIT.md
Normal file
58
docs/UI_UX_REDESIGN_AUDIT.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# UI/UX Redesign Audit
|
||||||
|
|
||||||
|
## 1. Что сейчас плохо
|
||||||
|
|
||||||
|
- Перегруженность: первый экран похож на engineering debug-dashboard.
|
||||||
|
- Horizontal scroll из-за `min-width: 1180px` и жёстких grid-колонок.
|
||||||
|
- Слабая адаптивность: mobile не в одну колонку, мелкие tap targets.
|
||||||
|
- Debug-dashboard вместо product demo: много панелей сразу.
|
||||||
|
- Мелкий текст (11–13px) и плотная типографика.
|
||||||
|
- Непонятный первый экран: не ясно, что это за система и как запустить демо.
|
||||||
|
|
||||||
|
## 2. Цель редизайна
|
||||||
|
|
||||||
|
Сделать чистый, адаптивный product demo для защиты хакатона: за 10 секунд понятно, что это, как запустить и какой результат показать. Инженерные панели сохранить, но спрятать ниже.
|
||||||
|
|
||||||
|
## 3. Новая информационная архитектура
|
||||||
|
|
||||||
|
1. Hero — смысл, CTA, цепочка Detection → Classification → Command → Routing
|
||||||
|
2. Product Demo — сцена + карточка результата + управление
|
||||||
|
3. Storyline Stepper — текущий этап цикла
|
||||||
|
4. Scenario Cards — jury-сценарии карточками
|
||||||
|
5. Criteria Cards — покрытие критериев OZON
|
||||||
|
6. Engineering Details — сворачиваемые техпанели
|
||||||
|
|
||||||
|
## 4. Компоненты: создать / изменить
|
||||||
|
|
||||||
|
| Компонент | Действие |
|
||||||
|
|-----------|----------|
|
||||||
|
| HeroSection | создать / доработать |
|
||||||
|
| ProductDemoSection | создать / доработать |
|
||||||
|
| StorylineStepper | создать / доработать |
|
||||||
|
| ScenarioCards | создать / доработать |
|
||||||
|
| CriteriaCards | создать / доработать |
|
||||||
|
| EngineeringDetails | создать / доработать |
|
||||||
|
| Header | упростить до product-nav |
|
||||||
|
| App | собрать product page |
|
||||||
|
| SorterScene | добавить `variant: full \| simple` |
|
||||||
|
| styles.css | responsive redesign |
|
||||||
|
|
||||||
|
## 5. В Engineering Details
|
||||||
|
|
||||||
|
StateMachinePanel, TimelinePanel, PidPanel, EventLog, MetricsPanel, SensorPanel, ScenarioPanel, OzonCriteriaPanel, SorterScene `variant="full"`.
|
||||||
|
|
||||||
|
## 6. Риски
|
||||||
|
|
||||||
|
- Не сломать domain logic / tests.
|
||||||
|
- Не потерять инженерные функции для жюри.
|
||||||
|
- Не затронуть nginx / соседние проекты.
|
||||||
|
- Сохранить обработчики start / step / reset / scenario select.
|
||||||
|
|
||||||
|
## 7. Definition of Done
|
||||||
|
|
||||||
|
- Нет horizontal scroll на 1920 / 1440 / 390.
|
||||||
|
- Hero понятен, CTA видны.
|
||||||
|
- Demo ведёт по цепочке Detection → Routing.
|
||||||
|
- Engineering Details свёрнуты по умолчанию.
|
||||||
|
- `npm run build` и `npm run test` проходят.
|
||||||
|
- Docker `owl` отдаёт страницу на `:3100` и доменах.
|
||||||
89
src/App.tsx
89
src/App.tsx
@@ -1,21 +1,27 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import Header from './components/Header';
|
import Header from './components/Header';
|
||||||
import HeroSection from './components/HeroSection';
|
import HeroSection from './components/HeroSection';
|
||||||
import ProductDemoSection from './components/ProductDemoSection';
|
import ProductDemoSection from './components/ProductDemoSection';
|
||||||
|
import StorylineStepper from './components/StorylineStepper';
|
||||||
import ScenarioCards from './components/ScenarioCards';
|
import ScenarioCards from './components/ScenarioCards';
|
||||||
import CriteriaCards from './components/CriteriaCards';
|
import CriteriaCards from './components/CriteriaCards';
|
||||||
import EngineeringDetails from './components/EngineeringDetails';
|
import EngineeringDetails, { type EngineeringDetailsHandle } from './components/EngineeringDetails';
|
||||||
import { DEMO_STEPS } from './data/demoSteps';
|
import { DEMO_STEPS } from './data/demoSteps';
|
||||||
import { SCENARIOS } from './data/scenarios';
|
import { SCENARIOS } from './data/scenarios';
|
||||||
import { createSimulation, setRunning, stepSimulationToNextState } from './domain/simulation';
|
import { createSimulation, stepSimulationToNextState } from './domain/simulation';
|
||||||
import type { ScenarioId, SimulationState } from './domain/types';
|
import type { ScenarioId, SimulationState } from './domain/types';
|
||||||
|
|
||||||
|
function scrollToId(id: string) {
|
||||||
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [activeScenarioId, setActiveScenarioId] = useState<ScenarioId>('normal_flow');
|
const [activeScenarioId, setActiveScenarioId] = useState<ScenarioId>('normal_flow');
|
||||||
const [demoStepIndex, setDemoStepIndex] = useState(0);
|
const [demoStepIndex, setDemoStepIndex] = useState(0);
|
||||||
|
const engineeringRef = useRef<EngineeringDetailsHandle>(null);
|
||||||
|
|
||||||
const activeScenario = useMemo(
|
const activeScenario = useMemo(
|
||||||
() => SCENARIOS.find((s) => s.id === activeScenarioId) ?? SCENARIOS[0],
|
() => SCENARIOS.find((scenario) => scenario.id === activeScenarioId) ?? SCENARIOS[0],
|
||||||
[activeScenarioId],
|
[activeScenarioId],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -26,28 +32,30 @@ export default function App() {
|
|||||||
setSimulation(createSimulation(activeScenario));
|
setSimulation(createSimulation(activeScenario));
|
||||||
}, [activeScenario]);
|
}, [activeScenario]);
|
||||||
|
|
||||||
// We are not using automatic interval in the new product demo by default to keep it step-by-step
|
|
||||||
// but if the user wants it to run automatically, we can preserve it if needed.
|
|
||||||
// Let's use handleNext to just step through.
|
|
||||||
|
|
||||||
const handleStartDemo = () => {
|
const handleStartDemo = () => {
|
||||||
document.getElementById('demo')?.scrollIntoView({ behavior: 'smooth' });
|
scrollToId('demo');
|
||||||
if (!simulation.currentItem && demoStepIndex === 0) {
|
setSimulation((current) => {
|
||||||
setSimulation(stepSimulationToNextState(simulation));
|
if (current.machineState === 'IDLE' && !current.currentItem) {
|
||||||
}
|
return stepSimulationToNextState(current);
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
// If simulation is finished or errored, we might need to reset or move to next scenario
|
const finished =
|
||||||
const isFinished = simulation.machineState === 'RETURN_HOME' || simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
simulation.machineState === 'RETURN_HOME' ||
|
||||||
|
simulation.machineState === 'FAULT' ||
|
||||||
|
simulation.machineState === 'EMERGENCY_STOP';
|
||||||
|
|
||||||
if (isFinished) {
|
if (finished) {
|
||||||
const nextIndex = (demoStepIndex + 1) % DEMO_STEPS.length;
|
const nextIndex = (demoStepIndex + 1) % DEMO_STEPS.length;
|
||||||
setDemoStepIndex(nextIndex);
|
setDemoStepIndex(nextIndex);
|
||||||
setActiveScenarioId(DEMO_STEPS[nextIndex].scenarioId);
|
setActiveScenarioId(DEMO_STEPS[nextIndex].scenarioId);
|
||||||
} else {
|
return;
|
||||||
setSimulation((current) => stepSimulationToNextState(current));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSimulation((current) => stepSimulationToNextState(current));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
@@ -56,39 +64,66 @@ export default function App() {
|
|||||||
|
|
||||||
const handleScenarioChange = (scenarioId: ScenarioId) => {
|
const handleScenarioChange = (scenarioId: ScenarioId) => {
|
||||||
setActiveScenarioId(scenarioId);
|
setActiveScenarioId(scenarioId);
|
||||||
// Find first step that matches this scenario
|
const stepIdx = DEMO_STEPS.findIndex((step) => step.scenarioId === scenarioId);
|
||||||
const stepIdx = DEMO_STEPS.findIndex(s => s.scenarioId === scenarioId);
|
if (stepIdx >= 0) {
|
||||||
if (stepIdx >= 0) setDemoStepIndex(stepIdx);
|
setDemoStepIndex(stepIdx);
|
||||||
document.getElementById('demo')?.scrollIntoView({ behavior: 'smooth' });
|
}
|
||||||
|
scrollToId('demo');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenEngineering = () => {
|
||||||
|
engineeringRef.current?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleShowScenarios = () => {
|
||||||
|
scrollToId('scenarios');
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="product-page">
|
<div className="product-page">
|
||||||
<Header simulation={simulation} />
|
<Header
|
||||||
|
simulation={simulation}
|
||||||
|
onReset={handleReset}
|
||||||
|
onOpenDemo={() => scrollToId('demo')}
|
||||||
|
onOpenScenarios={handleShowScenarios}
|
||||||
|
onOpenEngineering={handleOpenEngineering}
|
||||||
|
/>
|
||||||
|
|
||||||
<main className="product-main">
|
<main className="product-main">
|
||||||
<HeroSection onStartDemo={handleStartDemo} />
|
<HeroSection
|
||||||
|
onStartDemo={handleStartDemo}
|
||||||
|
onShowScenarios={handleShowScenarios}
|
||||||
|
onOpenEngineering={handleOpenEngineering}
|
||||||
|
/>
|
||||||
|
|
||||||
<ProductDemoSection
|
<ProductDemoSection
|
||||||
simulation={simulation}
|
simulation={simulation}
|
||||||
|
demoStepTitle={currentDemoStep.title}
|
||||||
|
onStartDemo={handleStartDemo}
|
||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
demoStepTitle={currentDemoStep.title}
|
onOpenEngineering={handleOpenEngineering}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<StorylineStepper simulation={simulation} />
|
||||||
|
|
||||||
<ScenarioCards
|
<ScenarioCards
|
||||||
scenarios={SCENARIOS}
|
scenarios={SCENARIOS}
|
||||||
activeScenarioId={activeScenarioId}
|
activeScenarioId={activeScenarioId}
|
||||||
onScenarioChange={handleScenarioChange}
|
onScenarioChange={handleScenarioChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CriteriaCards />
|
<CriteriaCards onLinkedScenario={handleScenarioChange} />
|
||||||
|
|
||||||
<EngineeringDetails simulation={simulation} />
|
<EngineeringDetails
|
||||||
|
ref={engineeringRef}
|
||||||
|
simulation={simulation}
|
||||||
|
onScenarioChange={handleScenarioChange}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="site-footer">
|
<footer className="site-footer">
|
||||||
<p>OZON Tech Hackathon 2026. Sorter Simulation.</p>
|
<p>OZON Tech Hackathon 2026 · Sorter Simulation · Product Demo</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +1,126 @@
|
|||||||
import { OZON_CRITERIA } from '../data/criteria';
|
import type { ScenarioId } from '../domain/types';
|
||||||
|
|
||||||
export default function CriteriaCards() {
|
interface CriteriaCard {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
evidence: string;
|
||||||
|
status: 'covered' | 'demo';
|
||||||
|
linkedScenario: ScenarioId;
|
||||||
|
linkedLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PRODUCT_CRITERIA: CriteriaCard[] = [
|
||||||
|
{
|
||||||
|
id: 'classification',
|
||||||
|
title: 'Классификация',
|
||||||
|
evidence: 'Rule-based дерево решений выдаёт категорию B/C/D для каждого SKU.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'normal_flow',
|
||||||
|
linkedLabel: 'Обычный товар',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dimensions',
|
||||||
|
title: 'Габариты',
|
||||||
|
evidence: 'Проверка min/max размеров до анализа формы.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'oversized_item',
|
||||||
|
linkedLabel: 'Негабарит',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'round-section',
|
||||||
|
title: 'Круг в сечении',
|
||||||
|
evidence: 'Roundness ≥ 0.8 направляет товар в зону D.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'round_object',
|
||||||
|
linkedLabel: 'Круглый объект',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rule-priority',
|
||||||
|
title: 'Приоритет правил',
|
||||||
|
evidence: 'Сначала габариты (C), затем форма (D), иначе основной поток B.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'boundary_dimensions',
|
||||||
|
linkedLabel: 'Пограничные размеры',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'routing',
|
||||||
|
title: 'Маршрутизация B/C/D',
|
||||||
|
evidence: 'Категория превращается в команду ROUTE_TO_* и физический маршрут.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'normal_flow',
|
||||||
|
linkedLabel: 'Обычный товар',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cycle-time',
|
||||||
|
title: 'Cycle time',
|
||||||
|
evidence: 'Метрики показывают длительность цикла и throughput.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'close_items',
|
||||||
|
linkedLabel: 'Очередь товаров',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'faults',
|
||||||
|
title: 'Нештатные ситуации',
|
||||||
|
evidence: 'Jam переводит систему в FAULT и останавливает конвейер.',
|
||||||
|
status: 'demo',
|
||||||
|
linkedScenario: 'jam',
|
||||||
|
linkedLabel: 'Застревание',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'safety',
|
||||||
|
title: 'Безопасность',
|
||||||
|
evidence: 'Emergency stop замораживает линию до Reset.',
|
||||||
|
status: 'demo',
|
||||||
|
linkedScenario: 'emergency_stop',
|
||||||
|
linkedLabel: 'Аварийная остановка',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'reproducibility',
|
||||||
|
title: 'Воспроизводимость',
|
||||||
|
evidence: 'Сценарии, тесты и docs позволяют повторить демо жюри.',
|
||||||
|
status: 'covered',
|
||||||
|
linkedScenario: 'low_confidence',
|
||||||
|
linkedLabel: 'Низкая уверенность CV',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onLinkedScenario?: (id: ScenarioId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CriteriaCards({ onLinkedScenario }: Props) {
|
||||||
return (
|
return (
|
||||||
<section className="criteria-cards-section" id="criteria">
|
<section className="criteria-cards-section" id="criteria" aria-labelledby="criteria-title">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h2>Покрытие критериев OZON</h2>
|
<h2 id="criteria-title">Критерии OZON</h2>
|
||||||
<p>Каждый инженерный критерий хакатона покрывается симуляцией и тестами.</p>
|
<p>Каждый критерий хакатона закрыт сценарием, симуляцией и тестами.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="criteria-grid">
|
<div className="criteria-grid">
|
||||||
{OZON_CRITERIA.map((criterion) => (
|
{PRODUCT_CRITERIA.map((criterion) => (
|
||||||
<div key={criterion.id} className="criteria-card">
|
<article key={criterion.id} className="criteria-card">
|
||||||
<div className="criteria-card-header">
|
<div className="criteria-card-header">
|
||||||
<h3>{criterion.title}</h3>
|
<h3>{criterion.title}</h3>
|
||||||
<span className={`status-badge ${criterion.status.replace(/ /g, '-')}`}>
|
<span className={`status-badge status-${criterion.status}`}>
|
||||||
{criterion.status}
|
{criterion.status === 'covered' ? 'Covered' : 'Demo'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="criteria-evidence">{criterion.evidence}</p>
|
<p className="criteria-evidence">{criterion.evidence}</p>
|
||||||
<div className="criteria-footer">
|
<div className="criteria-footer">
|
||||||
<span className="related-docs">Docs: {criterion.relatedDoc}</span>
|
<span>Сценарий:</span>
|
||||||
|
{onLinkedScenario ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="criteria-link"
|
||||||
|
onClick={() => onLinkedScenario(criterion.linkedScenario)}
|
||||||
|
>
|
||||||
|
{criterion.linkedLabel}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<strong>{criterion.linkedLabel}</strong>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { DIMENSION_LIMITS } from '../domain/classifier';
|
|
||||||
import type { SimulationState } from '../domain/types';
|
import type { SimulationState } from '../domain/types';
|
||||||
|
|
||||||
export default function CurrentProofCard({ simulation }: { simulation: SimulationState }) {
|
export default function CurrentProofCard({ simulation }: { simulation: SimulationState }) {
|
||||||
@@ -8,14 +7,19 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
|||||||
const dimensions = item?.dimensionsMm;
|
const dimensions = item?.dimensionsMm;
|
||||||
const lowConfidence = Boolean(item && item.confidence < 0.65);
|
const lowConfidence = Boolean(item && item.confidence < 0.65);
|
||||||
const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
||||||
|
const command = simulation.machineState.startsWith('ROUTE_TO_')
|
||||||
|
? simulation.machineState
|
||||||
|
: result
|
||||||
|
? `ROUTE_TO_${result.category}`
|
||||||
|
: 'WAITING';
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
return (
|
return (
|
||||||
<div className="proof-card proof-card-empty">
|
<div className="proof-card proof-card-empty">
|
||||||
<div className="proof-card-inner">
|
<div className="proof-card-inner">
|
||||||
<p className="eyebrow">Awaiting cycle</p>
|
<p className="eyebrow">Ожидание цикла</p>
|
||||||
<h3>Ready to demonstrate sorting cycle</h3>
|
<h3>Готово к демонстрации</h3>
|
||||||
<p>Нажмите «Запустить демо» для начала или переключите сценарий ниже.</p>
|
<p>Нажмите Start demo — товар пройдёт Detection → Classification → Command → Routing.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -25,33 +29,33 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
|||||||
<div className={`proof-card ${stopped ? 'proof-card-stopped' : ''}`}>
|
<div className={`proof-card ${stopped ? 'proof-card-stopped' : ''}`}>
|
||||||
<div className="proof-card-header">
|
<div className="proof-card-header">
|
||||||
<p className="eyebrow">Результат классификации</p>
|
<p className="eyebrow">Результат классификации</p>
|
||||||
<h3>{item?.name} (ID: {item?.id})</h3>
|
<h3>{item?.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="proof-grid">
|
<div className="proof-grid">
|
||||||
<div className="proof-item">
|
<div className="proof-item">
|
||||||
<span className="proof-label">Категория</span>
|
<span className="proof-label">Категория</span>
|
||||||
<strong className={`category-${result?.category || 'default'}`}>{result?.category || '-'}</strong>
|
<strong className={`category-${result?.category ?? 'default'}`}>{result?.category ?? '—'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="proof-item">
|
<div className="proof-item">
|
||||||
<span className="proof-label">Маршрут</span>
|
<span className="proof-label">Зона</span>
|
||||||
<strong>{result?.category ? `Зона ${result.category}` : '-'}</strong>
|
<strong>{result?.category ? `Зона ${result.category}` : '—'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="proof-item">
|
<div className="proof-item proof-item-wide">
|
||||||
<span className="proof-label">Команда</span>
|
<span className="proof-label">Команда</span>
|
||||||
<strong>{simulation.machineState.startsWith('ROUTE_TO_') ? simulation.machineState : 'WAITING'}</strong>
|
<strong>{command}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="proof-details">
|
<div className="proof-details">
|
||||||
<div className="detail-row">
|
<div className="detail-row">
|
||||||
<span>Габариты:</span>
|
<span>Габариты</span>
|
||||||
<strong className={result?.dimensionsPass === false ? 'fail-text' : ''}>
|
<strong className={result?.dimensionsPass === false ? 'fail-text' : ''}>
|
||||||
{dimensions?.width} x {dimensions?.depth} x {dimensions?.height} мм
|
{dimensions?.width} × {dimensions?.depth} × {dimensions?.height} мм
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-row">
|
<div className="detail-row">
|
||||||
<span>Roundness:</span>
|
<span>Roundness</span>
|
||||||
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : ''}>
|
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : ''}>
|
||||||
{item?.roundness.toFixed(2)}
|
{item?.roundness.toFixed(2)}
|
||||||
</strong>
|
</strong>
|
||||||
@@ -59,11 +63,14 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="proof-reason">
|
<div className="proof-reason">
|
||||||
<strong>Почему такое решение:</strong> {result?.reason}
|
<strong>Причина решения</strong>
|
||||||
{lowConfidence && <p className="warning-note">Низкая уверенность CV — использован rule-based fallback</p>}
|
<p>{result?.reason}</p>
|
||||||
{simulation.machineState === 'FAULT' && <p className="error-note">FAULT: Конвейер остановлен</p>}
|
{lowConfidence ? <p className="warning-note">Низкая уверенность CV — rule-based fallback</p> : null}
|
||||||
{simulation.machineState === 'EMERGENCY_STOP' && <p className="error-note">EMERGENCY_STOP: Аварийная остановка</p>}
|
{simulation.machineState === 'FAULT' ? <p className="error-note">FAULT: конвейер остановлен</p> : null}
|
||||||
{simulation.metrics.queueLength > 1 && <p className="warning-note">Внимание: очередь товаров</p>}
|
{simulation.machineState === 'EMERGENCY_STOP' ? (
|
||||||
|
<p className="error-note">EMERGENCY_STOP: аварийная остановка</p>
|
||||||
|
) : null}
|
||||||
|
{simulation.metrics.queueLength > 1 ? <p className="warning-note">Очередь товаров</p> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,107 @@
|
|||||||
import { useState } from 'react';
|
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
import StateMachinePanel from './StateMachinePanel';
|
import StateMachinePanel from './StateMachinePanel';
|
||||||
import TimelinePanel from './TimelinePanel';
|
import TimelinePanel from './TimelinePanel';
|
||||||
import PidPanel from './PidPanel';
|
import PidPanel from './PidPanel';
|
||||||
import EventLog from './EventLog';
|
import EventLog from './EventLog';
|
||||||
import MetricsPanel from './MetricsPanel';
|
import MetricsPanel from './MetricsPanel';
|
||||||
import SensorPanel from './SensorPanel';
|
import SensorPanel from './SensorPanel';
|
||||||
import type { SimulationState } from '../domain/types';
|
import ScenarioPanel from './ScenarioPanel';
|
||||||
|
import OzonCriteriaPanel from './OzonCriteriaPanel';
|
||||||
|
import SorterScene from './SorterScene';
|
||||||
|
import { DEMO_STEPS } from '../data/demoSteps';
|
||||||
|
import { SCENARIOS } from '../data/scenarios';
|
||||||
|
import type { ScenarioId, SimulationState } from '../domain/types';
|
||||||
|
|
||||||
export default function EngineeringDetails({ simulation }: { simulation: SimulationState }) {
|
export interface EngineeringDetailsHandle {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
open: () => void;
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="engineering-details-section" id="engineering">
|
|
||||||
<div className="engineering-toggle" onClick={() => setIsOpen(!isOpen)}>
|
|
||||||
<h2>Инженерные детали (Engineering Mode)</h2>
|
|
||||||
<button className="btn-secondary">
|
|
||||||
{isOpen ? 'Свернуть' : 'Развернуть'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div className="engineering-content">
|
|
||||||
<div className="engineering-grid">
|
|
||||||
<div className="eng-panel"><StateMachinePanel currentState={simulation.machineState} /></div>
|
|
||||||
<div className="eng-panel"><MetricsPanel metrics={simulation.metrics} /></div>
|
|
||||||
<div className="eng-panel"><SensorPanel simulation={simulation} /></div>
|
|
||||||
<div className="eng-panel"><TimelinePanel simulation={simulation} /></div>
|
|
||||||
<div className="eng-panel"><PidPanel pid={simulation.pid} metrics={simulation.metrics} status={simulation.systemStatus} /></div>
|
|
||||||
<div className="eng-panel"><EventLog events={simulation.events} /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
simulation: SimulationState;
|
||||||
|
onScenarioChange: (id: ScenarioId) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EngineeringDetails = forwardRef<EngineeringDetailsHandle, Props>(
|
||||||
|
function EngineeringDetails({ simulation, onScenarioChange }, ref) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const currentStep =
|
||||||
|
DEMO_STEPS.find((step) => step.scenarioId === simulation.scenario.id) ?? DEMO_STEPS[0];
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
open: () => {
|
||||||
|
setIsOpen(true);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document.getElementById('engineering')?.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="engineering-details-section" id="engineering" aria-labelledby="engineering-title">
|
||||||
|
<div className="engineering-toggle">
|
||||||
|
<div>
|
||||||
|
<p className="section-eyebrow">For experts</p>
|
||||||
|
<h2 id="engineering-title">Engineering Details</h2>
|
||||||
|
<p className="engineering-lead">
|
||||||
|
Полные технические панели: state machine, sensors, PID, timeline, event log и критерии.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={() => setIsOpen((value) => !value)}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls="engineering-content"
|
||||||
|
>
|
||||||
|
{isOpen ? 'Свернуть' : 'Открыть инженерный режим'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOpen ? (
|
||||||
|
<div className="engineering-content" id="engineering-content">
|
||||||
|
<div className="eng-panel eng-panel-wide">
|
||||||
|
<SorterScene simulation={simulation} variant="full" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="engineering-grid">
|
||||||
|
<div className="eng-panel">
|
||||||
|
<StateMachinePanel currentState={simulation.machineState} />
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel">
|
||||||
|
<MetricsPanel metrics={simulation.metrics} />
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel">
|
||||||
|
<SensorPanel simulation={simulation} />
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel">
|
||||||
|
<TimelinePanel simulation={simulation} />
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel">
|
||||||
|
<PidPanel
|
||||||
|
pid={simulation.pid}
|
||||||
|
metrics={simulation.metrics}
|
||||||
|
status={simulation.systemStatus}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel">
|
||||||
|
<ScenarioPanel
|
||||||
|
scenarios={SCENARIOS}
|
||||||
|
activeScenarioId={simulation.scenario.id}
|
||||||
|
onScenarioChange={onScenarioChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel eng-panel-wide">
|
||||||
|
<EventLog events={simulation.events} />
|
||||||
|
</div>
|
||||||
|
<div className="eng-panel eng-panel-wide">
|
||||||
|
<OzonCriteriaPanel currentStep={currentStep} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default EngineeringDetails;
|
||||||
|
|||||||
@@ -2,30 +2,49 @@ import type { SimulationState } from '../domain/types';
|
|||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
simulation: SimulationState;
|
simulation: SimulationState;
|
||||||
|
onReset: () => void;
|
||||||
|
onOpenDemo: () => void;
|
||||||
|
onOpenScenarios: () => void;
|
||||||
|
onOpenEngineering: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Header({ simulation }: HeaderProps) {
|
export default function Header({
|
||||||
|
simulation,
|
||||||
|
onReset,
|
||||||
|
onOpenDemo,
|
||||||
|
onOpenScenarios,
|
||||||
|
onOpenEngineering,
|
||||||
|
}: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<header className="site-header">
|
<header className="site-header">
|
||||||
<div className="header-logo">
|
<div className="header-logo">
|
||||||
<span className="logo-icon">O</span>
|
<span className="logo-icon" aria-hidden="true">
|
||||||
|
O
|
||||||
|
</span>
|
||||||
<div className="logo-text">
|
<div className="logo-text">
|
||||||
<span className="logo-title">OZON Tech</span>
|
<span className="logo-title">OZON Tech Sorter</span>
|
||||||
<span className="logo-subtitle">Sorter Simulation</span>
|
<span className="logo-subtitle">Product Demo</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="header-nav">
|
<nav className="header-nav" aria-label="Основная навигация">
|
||||||
<a href="#demo" className="nav-link">Демо</a>
|
<button type="button" className="nav-link" onClick={onOpenDemo}>
|
||||||
<a href="#scenarios" className="nav-link">Сценарии</a>
|
Демо
|
||||||
<a href="#criteria" className="nav-link">Критерии</a>
|
</button>
|
||||||
<a href="#engineering" className="nav-link">Инженерный режим</a>
|
<button type="button" className="nav-link" onClick={onOpenScenarios}>
|
||||||
</div>
|
Сценарии
|
||||||
|
</button>
|
||||||
|
<button type="button" className="nav-link" onClick={onOpenEngineering}>
|
||||||
|
Инженерный режим
|
||||||
|
</button>
|
||||||
|
<button type="button" className="nav-link nav-reset" onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div className="header-status-compact">
|
<div className="header-status-compact" aria-live="polite">
|
||||||
<div className={`status-dot ${simulation.systemStatus.toLowerCase()}`}></div>
|
<span className={`status-dot ${simulation.systemStatus.toLowerCase()}`} aria-hidden="true" />
|
||||||
<span>{simulation.systemStatus}</span>
|
<span>{simulation.systemStatus}</span>
|
||||||
<span className="sim-time">t+{(simulation.simTimeMs / 1000).toFixed(1)}s</span>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
export default function HeroSection({ onStartDemo }: { onStartDemo: () => void }) {
|
interface HeroSectionProps {
|
||||||
|
onStartDemo: () => void;
|
||||||
|
onShowScenarios: () => void;
|
||||||
|
onOpenEngineering: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HeroSection({
|
||||||
|
onStartDemo,
|
||||||
|
onShowScenarios,
|
||||||
|
onOpenEngineering,
|
||||||
|
}: HeroSectionProps) {
|
||||||
return (
|
return (
|
||||||
<section className="hero-section">
|
<section className="hero-section" aria-labelledby="hero-title">
|
||||||
<div className="hero-content">
|
<div className="hero-content">
|
||||||
<h1 className="hero-title">Интеллектуальная система сортировки товаров</h1>
|
<p className="hero-eyebrow">OZON Tech · Product Demo</p>
|
||||||
|
<h1 id="hero-title" className="hero-title">
|
||||||
|
Интеллектуальная система сортировки товаров
|
||||||
|
</h1>
|
||||||
<p className="hero-subtitle">
|
<p className="hero-subtitle">
|
||||||
Симуляция показывает полный цикл: обнаружение товара, классификация по правилам,
|
Симуляция показывает полный цикл: обнаружение товара, классификация по правилам,
|
||||||
управляющая команда и маршрутизация в зоны B, C, D.
|
управляющая команда и маршрутизация в зоны B/C/D.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="hero-badges">
|
<div className="hero-badges" aria-label="Ключевые возможности">
|
||||||
<span className="hero-badge">Computer Vision</span>
|
<span className="hero-badge">Computer Vision</span>
|
||||||
<span className="hero-badge">Rule-based classification</span>
|
<span className="hero-badge">Rule-based classification</span>
|
||||||
<span className="hero-badge">Actuator routing</span>
|
<span className="hero-badge">Actuator routing</span>
|
||||||
@@ -17,34 +30,43 @@ export default function HeroSection({ onStartDemo }: { onStartDemo: () => void }
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hero-actions">
|
<div className="hero-actions">
|
||||||
<button className="btn-primary" onClick={onStartDemo}>Запустить демо</button>
|
<button type="button" className="btn-primary" onClick={onStartDemo}>
|
||||||
<a href="#scenarios" className="btn-secondary">Показать сценарии</a>
|
Запустить демо
|
||||||
<a href="#engineering" className="btn-secondary">Инженерный режим</a>
|
</button>
|
||||||
|
<button type="button" className="btn-secondary" onClick={onShowScenarios}>
|
||||||
|
Показать сценарии
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn-secondary" onClick={onOpenEngineering}>
|
||||||
|
Инженерный режим
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="hero-visual">
|
<div className="hero-visual" aria-hidden="true">
|
||||||
<div className="hero-chain">
|
<div className="hero-chain">
|
||||||
<div className="chain-step">
|
<div className="chain-step">
|
||||||
<div className="chain-icon">📷</div>
|
<div className="chain-icon">1</div>
|
||||||
<span>Detection</span>
|
<span>Detection</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="chain-arrow">→</div>
|
<div className="chain-arrow" />
|
||||||
<div className="chain-step">
|
<div className="chain-step">
|
||||||
<div className="chain-icon">🧠</div>
|
<div className="chain-icon">2</div>
|
||||||
<span>Classification</span>
|
<span>Classification</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="chain-arrow">→</div>
|
<div className="chain-arrow" />
|
||||||
<div className="chain-step">
|
<div className="chain-step">
|
||||||
<div className="chain-icon">⚡</div>
|
<div className="chain-icon">3</div>
|
||||||
<span>Command</span>
|
<span>Command</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="chain-arrow">→</div>
|
<div className="chain-arrow" />
|
||||||
<div className="chain-step">
|
<div className="chain-step">
|
||||||
<div className="chain-icon">📦</div>
|
<div className="chain-icon">4</div>
|
||||||
<span>Routing</span>
|
<span>Routing</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="hero-chain-caption">
|
||||||
|
Результат демо: категория B/C/D, команда ROUTE_TO_* и целевая зона
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,40 +1,97 @@
|
|||||||
import SorterScene from './SorterScene';
|
import SorterScene from './SorterScene';
|
||||||
import CurrentProofCard from './CurrentProofCard';
|
import CurrentProofCard from './CurrentProofCard';
|
||||||
import StorylineStepper from './StorylineStepper';
|
|
||||||
import type { SimulationState } from '../domain/types';
|
import type { SimulationState } from '../domain/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
simulation: SimulationState;
|
simulation: SimulationState;
|
||||||
|
demoStepTitle: string;
|
||||||
|
onStartDemo: () => void;
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
demoStepTitle: string;
|
onOpenEngineering: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ProductDemoSection({ simulation, onNext, onReset, demoStepTitle }: Props) {
|
export default function ProductDemoSection({
|
||||||
|
simulation,
|
||||||
|
demoStepTitle,
|
||||||
|
onStartDemo,
|
||||||
|
onNext,
|
||||||
|
onReset,
|
||||||
|
onOpenEngineering,
|
||||||
|
}: Props) {
|
||||||
|
const item = simulation.currentItem;
|
||||||
|
const category = item?.classification.category;
|
||||||
|
const command = simulation.machineState.startsWith('ROUTE_TO_')
|
||||||
|
? simulation.machineState
|
||||||
|
: item?.classification
|
||||||
|
? `ROUTE_TO_${item.classification.category}`
|
||||||
|
: '—';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="product-demo-section" id="demo">
|
<section className="product-demo-section" id="demo" aria-labelledby="demo-title">
|
||||||
<div className="demo-header">
|
<div className="demo-header">
|
||||||
<h2>Live Demo</h2>
|
<div>
|
||||||
|
<p className="section-eyebrow">Live product demo</p>
|
||||||
|
<h2 id="demo-title">Главное демо</h2>
|
||||||
|
<p className="demo-lead">
|
||||||
|
Слева — упрощённая сцена конвейера. Справа — товар, категория, причина решения и команда маршрутизации.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="demo-status">
|
<div className="demo-status">
|
||||||
Текущий шаг: <strong>{demoStepTitle}</strong>
|
<span>Шаг:</span>
|
||||||
<span className={`status-dot ${simulation.systemStatus.toLowerCase()}`}></span>
|
<strong>{demoStepTitle}</strong>
|
||||||
|
<span className={`status-dot ${simulation.systemStatus.toLowerCase()}`} aria-hidden="true" />
|
||||||
|
<span className="demo-status-text">{simulation.systemStatus}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StorylineStepper simulation={simulation} />
|
|
||||||
|
|
||||||
<div className="demo-layout">
|
<div className="demo-layout">
|
||||||
<div className="demo-scene-container">
|
<div className="demo-scene-column">
|
||||||
<SorterScene simulation={simulation} />
|
<p className="demo-mobile-hint">
|
||||||
</div>
|
Товар проходит камеру, классификатор и gate, затем уходит в зону B, C или D.
|
||||||
|
</p>
|
||||||
<div className="demo-proof-container">
|
<div className="demo-scene-container">
|
||||||
<CurrentProofCard simulation={simulation} />
|
<SorterScene simulation={simulation} variant="simple" />
|
||||||
<div className="demo-controls">
|
|
||||||
<button className="btn-primary" onClick={onNext}>Следующий шаг (Next)</button>
|
|
||||||
<button className="btn-secondary" onClick={onReset}>Сброс (Reset)</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside className="demo-result-column" aria-label="Результат классификации">
|
||||||
|
<CurrentProofCard simulation={simulation} />
|
||||||
|
|
||||||
|
<div className="demo-result-summary">
|
||||||
|
<div className="summary-item">
|
||||||
|
<span>Товар</span>
|
||||||
|
<strong>{item?.item.name ?? 'Ожидание'}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<span>Категория</span>
|
||||||
|
<strong className={category ? `category-${category}` : ''}>{category ?? '—'}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<span>Команда</span>
|
||||||
|
<strong>{command}</strong>
|
||||||
|
</div>
|
||||||
|
<div className="summary-item">
|
||||||
|
<span>Зона</span>
|
||||||
|
<strong>{category ? `Зона ${category}` : '—'}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="demo-controls">
|
||||||
|
<button type="button" className="btn-primary" onClick={onStartDemo}>
|
||||||
|
Start demo
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn-primary" onClick={onNext}>
|
||||||
|
Next step
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn-secondary" onClick={onReset}>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button type="button" className="btn-secondary" onClick={onOpenEngineering}>
|
||||||
|
Engineering details
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,39 +6,97 @@ interface Props {
|
|||||||
onScenarioChange: (id: ScenarioId) => void;
|
onScenarioChange: (id: ScenarioId) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SCENARIO_UI: Record<
|
||||||
|
ScenarioId,
|
||||||
|
{ title: string; summary: string; badge: string; badgeTone: 'b' | 'c' | 'd' | 'mixed' | 'fault' }
|
||||||
|
> = {
|
||||||
|
normal_flow: {
|
||||||
|
title: 'Обычный товар',
|
||||||
|
summary: 'Стандартный поток с маршрутизацией в B, C и D.',
|
||||||
|
badge: 'B / C / D',
|
||||||
|
badgeTone: 'mixed',
|
||||||
|
},
|
||||||
|
oversized_item: {
|
||||||
|
title: 'Негабарит',
|
||||||
|
summary: 'Габариты превышают лимит — приоритет зоны C.',
|
||||||
|
badge: 'Зона C',
|
||||||
|
badgeTone: 'c',
|
||||||
|
},
|
||||||
|
round_object: {
|
||||||
|
title: 'Круглый объект',
|
||||||
|
summary: 'Габариты проходят, но круглое сечение ведёт в D.',
|
||||||
|
badge: 'Зона D',
|
||||||
|
badgeTone: 'd',
|
||||||
|
},
|
||||||
|
boundary_dimensions: {
|
||||||
|
title: 'Пограничные размеры',
|
||||||
|
summary: 'Проверка строгих min/max границ габаритов.',
|
||||||
|
badge: 'B / C',
|
||||||
|
badgeTone: 'mixed',
|
||||||
|
},
|
||||||
|
low_confidence: {
|
||||||
|
title: 'Низкая уверенность CV',
|
||||||
|
summary: 'Предупреждение CV и rule-based fallback.',
|
||||||
|
badge: 'Fallback',
|
||||||
|
badgeTone: 'b',
|
||||||
|
},
|
||||||
|
close_items: {
|
||||||
|
title: 'Очередь товаров',
|
||||||
|
summary: 'Близкие товары обрабатываются по одному циклу.',
|
||||||
|
badge: 'Queue',
|
||||||
|
badgeTone: 'mixed',
|
||||||
|
},
|
||||||
|
jam: {
|
||||||
|
title: 'Застревание',
|
||||||
|
summary: 'Jam у gate переводит линию в FAULT.',
|
||||||
|
badge: 'FAULT',
|
||||||
|
badgeTone: 'fault',
|
||||||
|
},
|
||||||
|
emergency_stop: {
|
||||||
|
title: 'Аварийная остановка',
|
||||||
|
summary: 'EMERGENCY_STOP останавливает движение до Reset.',
|
||||||
|
badge: 'E-STOP',
|
||||||
|
badgeTone: 'fault',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default function ScenarioCards({ scenarios, activeScenarioId, onScenarioChange }: Props) {
|
export default function ScenarioCards({ scenarios, activeScenarioId, onScenarioChange }: Props) {
|
||||||
return (
|
return (
|
||||||
<section className="scenario-cards-section" id="scenarios">
|
<section className="scenario-cards-section" id="scenarios" aria-labelledby="scenarios-title">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h2>Сценарии тестирования (Jury Test Cases)</h2>
|
<h2 id="scenarios-title">Сценарии</h2>
|
||||||
<p>Выберите сценарий для проверки поведения системы в разных условиях.</p>
|
<p>Выберите кейс жюри: обычный поток, негабарит, круглое сечение, очередь или авария.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scenario-grid">
|
<div className="scenario-grid">
|
||||||
{scenarios.map((scenario) => (
|
{scenarios.map((scenario) => {
|
||||||
<div
|
const ui = SCENARIO_UI[scenario.id];
|
||||||
key={scenario.id}
|
const active = scenario.id === activeScenarioId;
|
||||||
className={`scenario-card ${scenario.id === activeScenarioId ? 'active' : ''}`}
|
|
||||||
onClick={() => onScenarioChange(scenario.id)}
|
return (
|
||||||
>
|
<article
|
||||||
<div className="scenario-card-header">
|
key={scenario.id}
|
||||||
<h3>{scenario.name}</h3>
|
className={`scenario-card ${active ? 'active' : ''}`}
|
||||||
{scenario.id === activeScenarioId && <span className="active-badge">Выбран</span>}
|
>
|
||||||
</div>
|
<div className="scenario-card-header">
|
||||||
<p className="scenario-desc">{scenario.description}</p>
|
<h3>{ui.title}</h3>
|
||||||
<div className="scenario-meta">
|
<span className={`result-badge tone-${ui.badgeTone}`}>{ui.badge}</span>
|
||||||
<div className="meta-item">
|
|
||||||
<span className="meta-label">Цель:</span>
|
|
||||||
<span className="meta-value">{scenario.goal}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="meta-item">
|
<p className="scenario-desc">{ui.summary}</p>
|
||||||
<span className="meta-label">Результат:</span>
|
<p className="scenario-expected">
|
||||||
<span className="meta-value">{scenario.expectedCategorySummary}</span>
|
<span>Ожидаемый результат</span>
|
||||||
</div>
|
<strong>{scenario.expectedCategorySummary}</strong>
|
||||||
</div>
|
</p>
|
||||||
<button className="btn-secondary btn-small mt-auto">Применить</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
))}
|
className={active ? 'btn-primary' : 'btn-secondary'}
|
||||||
|
onClick={() => onScenarioChange(scenario.id)}
|
||||||
|
>
|
||||||
|
{active ? 'Выбран' : 'Показать'}
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -64,106 +64,172 @@ function LegendItem({ color, label }: { color: string; label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SorterScene({ simulation }: { simulation: SimulationState }) {
|
interface SorterSceneProps {
|
||||||
|
simulation: SimulationState;
|
||||||
|
variant?: 'full' | 'simple';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SorterScene({ simulation, variant = 'full' }: SorterSceneProps) {
|
||||||
const current = simulation.currentItem;
|
const current = simulation.currentItem;
|
||||||
const category = current?.classification.category;
|
const category = current?.classification.category;
|
||||||
const color = category ? routeColors[category] : '#38bdf8';
|
const color = category ? routeColors[category] : '#38bdf8';
|
||||||
const position = itemPosition(simulation);
|
const position = itemPosition(simulation);
|
||||||
const detecting = simulation.machineState === 'DETECTING';
|
const detecting = simulation.machineState === 'DETECTING';
|
||||||
|
const classifying = simulation.machineState === 'CLASSIFYING' || simulation.machineState === 'WAITING_AT_GATE';
|
||||||
const routeVisible = simulation.machineState.startsWith('ROUTE_TO_');
|
const routeVisible = simulation.machineState.startsWith('ROUTE_TO_');
|
||||||
const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
||||||
const itemWidth = current ? Math.max(24, Math.min(74, current.item.dimensionsMm.width / 6)) : 56;
|
const itemWidth = current ? Math.max(24, Math.min(74, current.item.dimensionsMm.width / 6)) : 56;
|
||||||
const itemHeight = current ? Math.max(18, Math.min(58, current.item.dimensionsMm.depth / 5.5)) : 48;
|
const itemHeight = current ? Math.max(18, Math.min(58, current.item.dimensionsMm.depth / 5.5)) : 48;
|
||||||
|
const isSimple = variant === 'simple';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scene-wrap">
|
<div className={`scene-wrap scene-${variant}`}>
|
||||||
<div className="scene-title-row">
|
{!isSimple ? (
|
||||||
<div>
|
<div className="scene-title-row">
|
||||||
<p className="eyebrow">Work zone 6000 x 10000 mm / conveyor 500 mm</p>
|
<div>
|
||||||
<h2>Engineering layout, sensors and routing commands</h2>
|
<p className="eyebrow">Work zone 6000 x 10000 mm / conveyor 500 mm</p>
|
||||||
|
<h2>Engineering layout, sensors and routing commands</h2>
|
||||||
|
</div>
|
||||||
|
<div className={`machine-state-chip ${stopped ? 'fault-chip' : ''}`}>{simulation.machineState}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`machine-state-chip ${stopped ? 'fault-chip' : ''}`}>{simulation.machineState}</div>
|
) : (
|
||||||
</div>
|
<div className="scene-simple-header">
|
||||||
|
<p className="eyebrow">Конвейер · камера · classifier · gate · зоны B/C/D</p>
|
||||||
|
<div className={`machine-state-chip ${stopped ? 'fault-chip' : ''}`}>{simulation.machineState}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<svg viewBox="0 0 1120 720" role="img" aria-label="Sorter simulation scene" className="sorter-svg">
|
<svg viewBox="0 0 1120 720" role="img" aria-label="Sorter simulation scene" className="sorter-svg">
|
||||||
<defs>
|
<defs>
|
||||||
<pattern id="gridMinor" width="24" height="24" patternUnits="userSpaceOnUse">
|
<pattern id={`gridMinor-${variant}`} width="24" height="24" patternUnits="userSpaceOnUse">
|
||||||
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#12283b" strokeWidth="1" />
|
<path d="M 24 0 L 0 0 0 24" fill="none" stroke="#12283b" strokeWidth="1" />
|
||||||
</pattern>
|
</pattern>
|
||||||
<pattern id="gridMajor" width="120" height="120" patternUnits="userSpaceOnUse">
|
<pattern id={`gridMajor-${variant}`} width="120" height="120" patternUnits="userSpaceOnUse">
|
||||||
<rect width="120" height="120" fill="url(#gridMinor)" />
|
<rect width="120" height="120" fill={`url(#gridMinor-${variant})`} />
|
||||||
<path d="M 120 0 L 0 0 0 120" fill="none" stroke="#244863" strokeWidth="1.4" />
|
<path d="M 120 0 L 0 0 0 120" fill="none" stroke="#244863" strokeWidth="1.4" />
|
||||||
</pattern>
|
</pattern>
|
||||||
{(['B', 'C', 'D'] as Category[]).map((route) => (
|
{(['B', 'C', 'D'] as Category[]).map((route) => (
|
||||||
<marker key={route} id={`arrow${route}`} markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
<marker key={route} id={`arrow${route}-${variant}`} markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
|
||||||
<path d="M0,0 L0,6 L9,3 z" fill={routeColors[route]} />
|
<path d="M0,0 L0,6 L9,3 z" fill={routeColors[route]} />
|
||||||
</marker>
|
</marker>
|
||||||
))}
|
))}
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<rect x="24" y="26" width="1072" height="632" rx="12" fill="#07111d" stroke="#244863" />
|
<rect x="24" y="26" width="1072" height="632" rx="12" fill="#07111d" stroke="#244863" />
|
||||||
<rect x="64" y="82" width="928" height="520" fill="url(#gridMajor)" opacity="0.9" />
|
{!isSimple ? (
|
||||||
<text x="78" y="74" className="scale-label left-label">Scaled plan: 6000 mm x 10000 mm work cell</text>
|
<rect x="64" y="82" width="928" height="520" fill={`url(#gridMajor-${variant})`} opacity="0.9" />
|
||||||
|
) : null}
|
||||||
|
|
||||||
<DimensionLine x1={64} y1={626} x2={992} y2={626} label="6000 mm work zone width" />
|
{!isSimple ? (
|
||||||
<DimensionLine x1={1024} y1={82} x2={1024} y2={602} label="10000 mm work zone length" />
|
<>
|
||||||
<DimensionLine x1={92} y1={314} x2={92} y2={406} label="500 mm conveyor" />
|
<text x="78" y="74" className="scale-label left-label">Scaled plan: 6000 mm x 10000 mm work cell</text>
|
||||||
|
<DimensionLine x1={64} y1={626} x2={992} y2={626} label="6000 mm work zone width" />
|
||||||
|
<DimensionLine x1={1024} y1={82} x2={1024} y2={602} label="10000 mm work zone length" />
|
||||||
|
<DimensionLine x1={92} y1={314} x2={92} y2={406} label="500 mm conveyor" />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<g className="zone zone-a">
|
<g className="zone zone-a">
|
||||||
<rect x="88" y="292" width="128" height="136" rx="8" />
|
<rect x="88" y="292" width="128" height="136" rx="8" />
|
||||||
<text x="152" y="282">A feed zone</text>
|
<text x="152" y={isSimple ? 370 : 282}>{isSimple ? 'A' : 'A feed zone'}</text>
|
||||||
</g>
|
</g>
|
||||||
<g className="zone zone-b">
|
<g className={`zone zone-b ${category === 'B' ? 'zone-active' : ''}`}>
|
||||||
<rect x="842" y="292" width="136" height="136" rx="8" />
|
<rect x="842" y="292" width="136" height="136" rx="8" />
|
||||||
<text x="910" y="282">B main sorter</text>
|
<text x="910" y={isSimple ? 370 : 282} className={isSimple ? 'zone-label-large' : undefined}>
|
||||||
|
{isSimple ? 'B' : 'B main sorter'}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g className="zone zone-d">
|
<g className={`zone zone-d ${category === 'D' ? 'zone-active' : ''}`}>
|
||||||
<rect x="642" y="112" width="210" height="124" rx="8" />
|
<rect x="642" y="112" width="210" height="124" rx="8" />
|
||||||
<text x="747" y="102">D roll-cage 1200 x 800 x 800 mm</text>
|
<text x="747" y={isSimple ? 185 : 102} className={isSimple ? 'zone-label-large' : undefined}>
|
||||||
|
{isSimple ? 'D' : 'D roll-cage 1200 x 800 x 800 mm'}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<g className="zone zone-c">
|
<g className={`zone zone-c ${category === 'C' ? 'zone-active' : ''}`}>
|
||||||
<rect x="642" y="486" width="210" height="124" rx="8" />
|
<rect x="642" y="486" width="210" height="124" rx="8" />
|
||||||
<text x="747" y="632">C roll-cage 1200 x 800 x 800 mm</text>
|
<text x="747" y={isSimple ? 560 : 632} className={isSimple ? 'zone-label-large' : undefined}>
|
||||||
|
{isSimple ? 'C' : 'C roll-cage 1200 x 800 x 800 mm'}
|
||||||
|
</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<rect className={stopped ? 'conveyor stopped' : 'conveyor'} x="106" y="314" width="850" height="92" rx="6" />
|
<rect className={stopped ? 'conveyor stopped' : 'conveyor'} x="106" y="314" width="850" height="92" rx="6" />
|
||||||
<line x1="126" y1="360" x2="936" y2="360" className="belt-center" />
|
<line x1="126" y1="360" x2="936" y2="360" className="belt-center" />
|
||||||
{Array.from({ length: 18 }).map((_, index) => (
|
{!isSimple
|
||||||
<line key={index} x1={132 + index * 44} y1="324" x2={158 + index * 44} y2="396" className="roller-line" />
|
? Array.from({ length: 18 }).map((_, index) => (
|
||||||
))}
|
<line key={index} x1={132 + index * 44} y1="324" x2={158 + index * 44} y2="396" className="roller-line" />
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
|
||||||
<g className={simulation.sensors.camera.active ? 'device active' : 'device'}>
|
<g className={simulation.sensors.camera.active ? 'device active' : 'device'}>
|
||||||
<rect x="350" y="218" width="76" height="50" rx="6" />
|
<rect x="350" y="218" width="76" height="50" rx="6" />
|
||||||
<line x1="388" y1="268" x2="388" y2="314" />
|
<line x1="388" y1="268" x2="388" y2="314" />
|
||||||
<text x="388" y="208">Camera / bbox</text>
|
<text x="388" y="208">{isSimple ? 'Camera' : 'Camera / bbox'}</text>
|
||||||
</g>
|
|
||||||
<g className={simulation.sensors.laser.active ? 'device active' : 'device'}>
|
|
||||||
<rect x="474" y="218" width="76" height="50" rx="6" />
|
|
||||||
<line x1="512" y1="268" x2="512" y2="314" />
|
|
||||||
<text x="512" y="208">Laser height</text>
|
|
||||||
</g>
|
|
||||||
<g className={simulation.sensors.ultrasound.active ? 'device active' : 'device'}>
|
|
||||||
<circle cx="646" cy="243" r="28" />
|
|
||||||
<line x1="646" y1="271" x2="646" y2="314" />
|
|
||||||
<text x="646" y="208">Ultrasonic gate</text>
|
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
{!isSimple ? (
|
||||||
|
<>
|
||||||
|
<g className={simulation.sensors.laser.active ? 'device active' : 'device'}>
|
||||||
|
<rect x="474" y="218" width="76" height="50" rx="6" />
|
||||||
|
<line x1="512" y1="268" x2="512" y2="314" />
|
||||||
|
<text x="512" y="208">Laser height</text>
|
||||||
|
</g>
|
||||||
|
<g className={simulation.sensors.ultrasound.active ? 'device active' : 'device'}>
|
||||||
|
<circle cx="646" cy="243" r="28" />
|
||||||
|
<line x1="646" y1="271" x2="646" y2="314" />
|
||||||
|
<text x="646" y="208">Ultrasonic gate</text>
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<g className={classifying || simulation.sensors.ultrasound.active ? 'device active' : 'device'}>
|
||||||
|
<rect x="520" y="218" width="110" height="50" rx="6" />
|
||||||
|
<line x1="575" y1="268" x2="575" y2="314" />
|
||||||
|
<text x="575" y="208">Classifier</text>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
|
|
||||||
<g className={simulation.gate.open ? 'gate open' : 'gate closed'}>
|
<g className={simulation.gate.open ? 'gate open' : 'gate closed'}>
|
||||||
<line x1="708" y1="296" x2="708" y2="424" />
|
<line x1="708" y1="296" x2="708" y2="424" />
|
||||||
<text x="746" y="300">Stop-gate {simulation.gate.open ? 'open' : 'closed'}</text>
|
<text x="746" y="300">{isSimple ? 'Gate' : `Stop-gate ${simulation.gate.open ? 'open' : 'closed'}`}</text>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g className={`pusher ${simulation.actuators.pusherC}`}>
|
{!isSimple ? (
|
||||||
<rect x="625" y="424" width="174" height="34" rx="6" />
|
<>
|
||||||
<text x="712" y="476">Pusher C command</text>
|
<g className={`pusher ${simulation.actuators.pusherC}`}>
|
||||||
</g>
|
<rect x="625" y="424" width="174" height="34" rx="6" />
|
||||||
<g className={`pusher ${simulation.actuators.pusherD}`}>
|
<text x="712" y="476">Pusher C command</text>
|
||||||
<rect x="625" y="262" width="174" height="34" rx="6" />
|
</g>
|
||||||
<text x="712" y="256">Pusher D command</text>
|
<g className={`pusher ${simulation.actuators.pusherD}`}>
|
||||||
</g>
|
<rect x="625" y="262" width="174" height="34" rx="6" />
|
||||||
|
<text x="712" y="256">Pusher D command</text>
|
||||||
|
</g>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<line
|
||||||
|
x1="706"
|
||||||
|
y1="360"
|
||||||
|
x2="928"
|
||||||
|
y2="360"
|
||||||
|
className={`route-guide route-b ${category === 'B' && routeVisible ? 'route-active' : ''}`}
|
||||||
|
markerEnd={`url(#arrowB-${variant})`}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="706"
|
||||||
|
y1="376"
|
||||||
|
x2="748"
|
||||||
|
y2="548"
|
||||||
|
className={`route-guide route-c ${category === 'C' && routeVisible ? 'route-active' : ''}`}
|
||||||
|
markerEnd={`url(#arrowC-${variant})`}
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="706"
|
||||||
|
y1="344"
|
||||||
|
x2="748"
|
||||||
|
y2="174"
|
||||||
|
className={`route-guide route-d ${category === 'D' && routeVisible ? 'route-active' : ''}`}
|
||||||
|
markerEnd={`url(#arrowD-${variant})`}
|
||||||
|
/>
|
||||||
|
|
||||||
<line x1="706" y1="360" x2="928" y2="360" className="route-guide route-b" markerEnd="url(#arrowB)" />
|
|
||||||
<line x1="706" y1="376" x2="748" y2="548" className="route-guide route-c" markerEnd="url(#arrowC)" />
|
|
||||||
<line x1="706" y1="344" x2="748" y2="174" className="route-guide route-d" markerEnd="url(#arrowD)" />
|
|
||||||
{routeVisible && category ? (
|
{routeVisible && category ? (
|
||||||
<g className="route-command">
|
<g className="route-command">
|
||||||
<rect x="788" y="326" width="156" height="34" rx="8" fill={routeColors[category]} />
|
<rect x="788" y="326" width="156" height="34" rx="8" fill={routeColors[category]} />
|
||||||
@@ -184,11 +250,22 @@ export default function SorterScene({ simulation }: { simulation: SimulationStat
|
|||||||
stroke="#ffffff"
|
stroke="#ffffff"
|
||||||
strokeWidth="1.4"
|
strokeWidth="1.4"
|
||||||
/>
|
/>
|
||||||
<text x={position.x} y={position.y + itemHeight / 2 + 18} className="item-label">{current.item.id}</text>
|
<text x={position.x} y={position.y + itemHeight / 2 + 18} className="item-label">
|
||||||
|
{isSimple ? current.item.name : current.item.id}
|
||||||
|
</text>
|
||||||
{detecting ? (
|
{detecting ? (
|
||||||
<g className="bbox">
|
<g className="bbox">
|
||||||
<rect x={position.x - itemWidth / 2 - 10} y={position.y - itemHeight / 2 - 10} width={itemWidth + 20} height={itemHeight + 20} />
|
<rect
|
||||||
<text x={position.x} y={position.y - itemHeight / 2 - 18}>bbox {current.item.dimensionsMm.width} x {current.item.dimensionsMm.depth} mm</text>
|
x={position.x - itemWidth / 2 - 10}
|
||||||
|
y={position.y - itemHeight / 2 - 10}
|
||||||
|
width={itemWidth + 20}
|
||||||
|
height={itemHeight + 20}
|
||||||
|
/>
|
||||||
|
{!isSimple ? (
|
||||||
|
<text x={position.x} y={position.y - itemHeight / 2 - 18}>
|
||||||
|
bbox {current.item.dimensionsMm.width} x {current.item.dimensionsMm.depth} mm
|
||||||
|
</text>
|
||||||
|
) : null}
|
||||||
</g>
|
</g>
|
||||||
) : null}
|
) : null}
|
||||||
</g>
|
</g>
|
||||||
@@ -202,13 +279,23 @@ export default function SorterScene({ simulation }: { simulation: SimulationStat
|
|||||||
</g>
|
</g>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<g className="scene-legend" transform="translate(78 664)">
|
{!isSimple ? (
|
||||||
<LegendItem color="#4ade80" label="B main sorter" />
|
<g className="scene-legend" transform="translate(78 664)">
|
||||||
<g transform="translate(140 0)"><LegendItem color="#f59e0b" label="C oversize" /></g>
|
<LegendItem color="#4ade80" label="B main sorter" />
|
||||||
<g transform="translate(270 0)"><LegendItem color="#c084fc" label="D shape / repack" /></g>
|
<g transform="translate(140 0)"><LegendItem color="#f59e0b" label="C oversize" /></g>
|
||||||
<g transform="translate(430 0)"><LegendItem color="#38bdf8" label="camera / laser / ultrasonic active" /></g>
|
<g transform="translate(270 0)"><LegendItem color="#c084fc" label="D shape / repack" /></g>
|
||||||
<g transform="translate(700 0)"><LegendItem color="#fb3d4e" label="stop-gate / fault" /></g>
|
<g transform="translate(430 0)"><LegendItem color="#38bdf8" label="camera / laser / ultrasonic active" /></g>
|
||||||
</g>
|
<g transform="translate(700 0)"><LegendItem color="#fb3d4e" label="stop-gate / fault" /></g>
|
||||||
|
</g>
|
||||||
|
) : (
|
||||||
|
<g className="scene-legend" transform="translate(78 664)">
|
||||||
|
<LegendItem color="#4ade80" label="B" />
|
||||||
|
<g transform="translate(70 0)"><LegendItem color="#f59e0b" label="C" /></g>
|
||||||
|
<g transform="translate(140 0)"><LegendItem color="#c084fc" label="D" /></g>
|
||||||
|
<g transform="translate(210 0)"><LegendItem color="#38bdf8" label="active" /></g>
|
||||||
|
<g transform="translate(320 0)"><LegendItem color="#fb3d4e" label="fault" /></g>
|
||||||
|
</g>
|
||||||
|
)}
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,65 +1,76 @@
|
|||||||
import type { SimulationState } from '../domain/types';
|
import type { MachineState, SimulationState } from '../domain/types';
|
||||||
|
|
||||||
|
type StepKey = 'detection' | 'classification' | 'decision' | 'command' | 'routing';
|
||||||
|
type StepStatus = 'active' | 'completed' | 'pending';
|
||||||
|
|
||||||
|
const STEPS: Array<{ key: StepKey; number: number; label: string; description: string }> = [
|
||||||
|
{ key: 'detection', number: 1, label: 'Detection', description: 'Камера и датчики снимают габариты' },
|
||||||
|
{ key: 'classification', number: 2, label: 'Classification', description: 'Правила проверяют размеры и форму' },
|
||||||
|
{ key: 'decision', number: 3, label: 'Decision', description: 'Система выбирает категорию B/C/D' },
|
||||||
|
{ key: 'command', number: 4, label: 'Command', description: 'Формируется команда ROUTE_TO_*' },
|
||||||
|
{ key: 'routing', number: 5, label: 'Routing', description: 'Товар уходит в целевую зону' },
|
||||||
|
];
|
||||||
|
|
||||||
|
function activeStep(state: MachineState): StepKey | null {
|
||||||
|
if (state === 'IDLE') return null;
|
||||||
|
if (state === 'MOVING_TO_CAMERA' || state === 'DETECTING') return 'detection';
|
||||||
|
if (state === 'MOVING_TO_GATE' || state === 'WAITING_AT_GATE') return 'classification';
|
||||||
|
if (state === 'CLASSIFYING') return 'decision';
|
||||||
|
if (state.startsWith('ROUTE_TO_')) return 'command';
|
||||||
|
if (state === 'RETURN_HOME') return 'routing';
|
||||||
|
if (state === 'FAULT' || state === 'EMERGENCY_STOP') return 'routing';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stepStatus(key: StepKey, current: StepKey | null, machineState: MachineState): StepStatus {
|
||||||
|
if (machineState === 'IDLE') return 'pending';
|
||||||
|
|
||||||
|
const order: StepKey[] = ['detection', 'classification', 'decision', 'command', 'routing'];
|
||||||
|
const currentIndex = current ? order.indexOf(current) : -1;
|
||||||
|
const keyIndex = order.indexOf(key);
|
||||||
|
|
||||||
|
if (machineState === 'RETURN_HOME') {
|
||||||
|
return keyIndex < order.length - 1 ? 'completed' : 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (machineState === 'FAULT' || machineState === 'EMERGENCY_STOP') {
|
||||||
|
return keyIndex <= currentIndex ? 'active' : 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyIndex < currentIndex) return 'completed';
|
||||||
|
if (keyIndex === currentIndex) return 'active';
|
||||||
|
return 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
export default function StorylineStepper({ simulation }: { simulation: SimulationState }) {
|
export default function StorylineStepper({ simulation }: { simulation: SimulationState }) {
|
||||||
const state = simulation.machineState;
|
const current = activeStep(simulation.machineState);
|
||||||
|
|
||||||
const isDetection = state === 'MOVING_TO_CAMERA' || state === 'DETECTING';
|
|
||||||
const isClassification = state === 'MOVING_TO_GATE' || state === 'WAITING_AT_GATE' || state === 'CLASSIFYING';
|
|
||||||
const isCommand = state.startsWith('ROUTE_TO_');
|
|
||||||
const isRouting = state === 'RETURN_HOME';
|
|
||||||
|
|
||||||
const item = simulation.currentItem;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="storyline-stepper">
|
<section className="storyline-section" id="storyline" aria-labelledby="storyline-title">
|
||||||
<Step
|
<div className="section-header">
|
||||||
label="1. Detection"
|
<h2 id="storyline-title">Этапы цикла</h2>
|
||||||
desc="Распознавание габаритов"
|
<p>Текущий этап системы в цепочке Detection → Classification → Decision → Command → Routing.</p>
|
||||||
active={isDetection}
|
</div>
|
||||||
done={Boolean(item) && !isDetection}
|
|
||||||
/>
|
<ol className="storyline-stepper">
|
||||||
<Step
|
{STEPS.map((step) => {
|
||||||
label="2. Classification"
|
const status = stepStatus(step.key, current, simulation.machineState);
|
||||||
desc="Дерево решений"
|
return (
|
||||||
active={isClassification}
|
<li key={step.key} className={`story-step step-${status}`}>
|
||||||
done={Boolean(item?.classification) && !isClassification && !isDetection}
|
<div className="step-indicator" aria-hidden="true">
|
||||||
/>
|
<span className="step-number">{step.number}</span>
|
||||||
<Step
|
</div>
|
||||||
label="3. Decision"
|
<div className="step-content">
|
||||||
desc="Выбор категории"
|
<strong>{step.label}</strong>
|
||||||
active={isClassification && state === 'CLASSIFYING'}
|
<span>{step.description}</span>
|
||||||
done={Boolean(item?.classification)}
|
<span className="step-status-label">
|
||||||
/>
|
{status === 'active' ? 'Сейчас' : status === 'completed' ? 'Готово' : 'Ожидание'}
|
||||||
<Step
|
</span>
|
||||||
label="4. Command"
|
</div>
|
||||||
desc="Сигнал механизмам"
|
</li>
|
||||||
active={isCommand}
|
);
|
||||||
done={isRouting}
|
})}
|
||||||
/>
|
</ol>
|
||||||
<Step
|
</section>
|
||||||
label="5. Routing"
|
|
||||||
desc="Движение в зону"
|
|
||||||
active={isCommand || isRouting}
|
|
||||||
done={isRouting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Step({ label, desc, active, done }: { label: string, desc: string, active: boolean, done: boolean }) {
|
|
||||||
let statusClass = 'step-pending';
|
|
||||||
if (active) statusClass = 'step-active';
|
|
||||||
else if (done) statusClass = 'step-done';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`story-step ${statusClass}`}>
|
|
||||||
<div className="step-indicator">
|
|
||||||
<div className="step-dot"></div>
|
|
||||||
</div>
|
|
||||||
<div className="step-content">
|
|
||||||
<strong>{label}</strong>
|
|
||||||
<span>{desc}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
3277
src/styles.css
3277
src/styles.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user