feat: align project with OZON Track 3 official specification
- Fix critical parameters: min dimensions 10×10×2 mm (was 10×10×10) - Fix roundness threshold: K ≥ 0.7 (was 0.8) - Add input_info analysis document (INPUT_INFO_ANALYSIS.md) - Update Hero: new title, badges (Min 10×10×2, K≥0.7→D, C priority), proof-line - Add CurrentProofCard summary block: Category/Command/Target/Why (cyan border) - Add CPriorityExplanation component (orange card, dedicated section) - Update all docs: README, DEMO_SCRIPT (7-min timing), JURY_QA, SUBMISSION_CHECKLIST - Update tests: 16/16 passed under new parameters - Update 3D twin: dynamic DIMENSION_LIMITS display - Update criteria cards: roundness ≥ 0.7 - Tested: build OK, Docker OK, domains OK (arhipovdan.ru, www, ai-shorts.ru)
This commit is contained in:
@@ -4,6 +4,7 @@ import HeroSection from './components/HeroSection';
|
||||
import ProductDemoSection from './components/ProductDemoSection';
|
||||
import StorylineStepper from './components/StorylineStepper';
|
||||
import ScenarioCards from './components/ScenarioCards';
|
||||
import CPriorityExplanation from './components/CPriorityExplanation';
|
||||
import CriteriaCards from './components/CriteriaCards';
|
||||
import EngineeringDetails, { type EngineeringDetailsHandle } from './components/EngineeringDetails';
|
||||
import { DEMO_STEPS } from './data/demoSteps';
|
||||
@@ -79,6 +80,10 @@ export default function App() {
|
||||
scrollToId('scenarios');
|
||||
};
|
||||
|
||||
const handleShowCPriority = () => {
|
||||
handleScenarioChange('c_priority');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="product-page">
|
||||
<Header
|
||||
@@ -113,6 +118,8 @@ export default function App() {
|
||||
onScenarioChange={handleScenarioChange}
|
||||
/>
|
||||
|
||||
<CPriorityExplanation onShowCPriority={handleShowCPriority} />
|
||||
|
||||
<CriteriaCards onLinkedScenario={handleScenarioChange} />
|
||||
|
||||
<EngineeringDetails
|
||||
|
||||
26
src/components/CPriorityExplanation.tsx
Normal file
26
src/components/CPriorityExplanation.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
interface CPriorityExplanationProps {
|
||||
onShowCPriority: () => void;
|
||||
}
|
||||
|
||||
export default function CPriorityExplanation({ onShowCPriority }: CPriorityExplanationProps) {
|
||||
return (
|
||||
<section className="c-priority-section" aria-labelledby="c-priority-title">
|
||||
<div className="c-priority-card">
|
||||
<div className="c-priority-icon" aria-hidden="true">
|
||||
C
|
||||
</div>
|
||||
<div className="c-priority-content">
|
||||
<h3 id="c-priority-title">Почему C имеет приоритет?</h3>
|
||||
<p>
|
||||
Если товар одновременно негабаритный и имеет круг в сечении, он направляется в C.
|
||||
Сначала проверяются габариты, и только если они проходят — форма.
|
||||
Это предотвращает попадание негабаритных объектов в основной сортировщик.
|
||||
</p>
|
||||
<button type="button" className="btn-primary" onClick={onShowCPriority}>
|
||||
Показать C-priority
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,7 @@ const PRODUCT_CRITERIA: CriteriaCard[] = [
|
||||
{
|
||||
id: 'round-section',
|
||||
title: 'Круг в сечении',
|
||||
evidence: 'Roundness ≥ 0.8 направляет товар в зону D.',
|
||||
evidence: 'Roundness ≥ 0.7 направляет товар в зону D.',
|
||||
status: 'covered',
|
||||
linkedScenario: 'round_object',
|
||||
linkedLabel: 'Круглый объект',
|
||||
|
||||
@@ -40,6 +40,33 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
||||
<h3>{item?.name}</h3>
|
||||
</div>
|
||||
|
||||
<div className="proof-summary">
|
||||
<div className="summary-row">
|
||||
<span className="summary-label">Category:</span>
|
||||
<strong className={`category-${result?.category ?? 'default'}`}>{result?.category ?? '—'}</strong>
|
||||
</div>
|
||||
<div className="summary-row">
|
||||
<span className="summary-label">Command:</span>
|
||||
<strong>{command}</strong>
|
||||
</div>
|
||||
<div className="summary-row">
|
||||
<span className="summary-label">Target:</span>
|
||||
<strong>{result?.category ? `Zone ${result.category}` : '—'}</strong>
|
||||
</div>
|
||||
<div className="summary-row">
|
||||
<span className="summary-label">Why:</span>
|
||||
<em>
|
||||
{isCPriority
|
||||
? `dimensions failed, so C has priority even though K = ${item?.roundness.toFixed(2)}`
|
||||
: result?.dimensionsPass === false
|
||||
? 'dimensions failed (oversized or undersized)'
|
||||
: result?.roundnessPass === false
|
||||
? `dimensions pass, but K = ${item?.roundness.toFixed(2)} ≥ 0.7`
|
||||
: 'dimensions pass and round section is not detected'}
|
||||
</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="proof-grid">
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">Dimensions</span>
|
||||
@@ -55,7 +82,7 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
|
||||
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : 'pass-text'}>
|
||||
{item?.roundness.toFixed(2)}
|
||||
</strong>
|
||||
<em>{result?.roundnessPass ? 'PASS' : 'DETECTED (≥0.8)'}</em>
|
||||
<em>{result?.roundnessPass ? 'PASS' : 'DETECTED (≥0.7)'}</em>
|
||||
</div>
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">Category</span>
|
||||
|
||||
@@ -14,19 +14,18 @@ export default function HeroSection({
|
||||
<div className="hero-content">
|
||||
<p className="hero-eyebrow">OZON Tech · Product Demo</p>
|
||||
<h1 id="hero-title" className="hero-title">
|
||||
Интеллектуальная система сортировки товаров
|
||||
Интеллектуальная система предварительной сортировки товаров
|
||||
</h1>
|
||||
<p className="hero-subtitle">
|
||||
Симуляция показывает полный цикл: обнаружение товара, классификация по правилам,
|
||||
управляющая команда и маршрутизация в зоны B/C/D.
|
||||
3D-симуляция ПАК показывает полный цикл: товар поступает из A, определяется CV-модулем, классифицируется в B/C/D и физически маршрутизируется исполнительной частью.
|
||||
</p>
|
||||
|
||||
<div className="hero-badges" aria-label="Ключевые возможности">
|
||||
<span className="hero-badge">Computer Vision</span>
|
||||
<span className="hero-badge">Rule-based classification</span>
|
||||
<span className="hero-badge">Actuator routing</span>
|
||||
<span className="hero-badge">Cycle time</span>
|
||||
<span className="hero-badge">Fault handling</span>
|
||||
<div className="hero-badges" aria-label="Параметры системы">
|
||||
<span className="hero-badge">Конвейер 1 м/с</span>
|
||||
<span className="hero-badge">Min 10×10×2 мм</span>
|
||||
<span className="hero-badge">Max 450×320×320 мм</span>
|
||||
<span className="hero-badge">K ≥ 0.7 → D</span>
|
||||
<span className="hero-badge">C priority</span>
|
||||
</div>
|
||||
|
||||
<div className="hero-actions">
|
||||
@@ -65,7 +64,7 @@ export default function HeroSection({
|
||||
</div>
|
||||
</div>
|
||||
<p className="hero-chain-caption">
|
||||
Результат демо: категория B/C/D, команда ROUTE_TO_* и целевая зона
|
||||
Detection → Classification → ROUTE_TO_* → Actuator → B/C/D
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -11,6 +11,7 @@ import RouteArrows3D from './RouteArrows3D';
|
||||
import SceneLabels3D from './SceneLabels3D';
|
||||
import { PHYSICS_ENGINE_ENABLED } from './itemMotion';
|
||||
import { NOMINAL_CONVEYOR_SPEED_MPS } from '../../domain/simulation';
|
||||
import { DIMENSION_LIMITS } from '../../domain/classifier';
|
||||
|
||||
export interface SorterDigitalTwinProps {
|
||||
simulation: SimulationState;
|
||||
@@ -142,7 +143,9 @@ export default function SorterDigitalTwin({
|
||||
{category ? <span className={`twin-chip category-${category}`}>Zone {category}</span> : null}
|
||||
<span className="twin-chip">{command}</span>
|
||||
<span className="twin-chip">Conveyor {NOMINAL_CONVEYOR_SPEED_MPS.toFixed(2)} m/s</span>
|
||||
<span className="twin-chip">Min 10×10×10 mm</span>
|
||||
<span className="twin-chip">
|
||||
Min {DIMENSION_LIMITS.min.width}×{DIMENSION_LIMITS.min.depth}×{DIMENSION_LIMITS.min.height} mm
|
||||
</span>
|
||||
{showFps ? <span className="twin-chip">FPS ~{fps}</span> : null}
|
||||
<span className="twin-chip muted-chip">
|
||||
Motion: {PHYSICS_ENGINE_ENABLED ? 'physics' : 'state-machine'}
|
||||
|
||||
@@ -43,7 +43,7 @@ export const DEMO_STEPS: DemoStep[] = [
|
||||
scenarioId: 'round_object',
|
||||
preferredAction: 'step',
|
||||
focusArea: 'scene',
|
||||
explanation: 'Roundness >= 0.8 after valid dimensions sends the item to D for repack/shape issue handling.',
|
||||
explanation: 'Roundness >= 0.7 after valid dimensions sends the item to D for repack/shape issue handling.',
|
||||
whatToWatch: 'Roundness FAIL for B, category D and the upper route/pusher D command.',
|
||||
juryValue: 'Shows how the circle-section rule becomes a physical route.',
|
||||
relatedCriteria: ['classification-rules', 'geometry-variety', 'physical-routing'],
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SCENARIOS: Scenario[] = [
|
||||
{
|
||||
id: 'round_object',
|
||||
name: 'Round object',
|
||||
description: 'Габариты проходят, но roundness >= 0.8, маршрут в D.',
|
||||
description: 'Габариты проходят, но roundness >= 0.7, маршрут в D.',
|
||||
goal: 'Показать проверку круглого сечения после габаритов.',
|
||||
expectedCategorySummary: 'D for every item',
|
||||
demonstrates: 'Rule-based shape issue routing без реального ML на MVP-этапе.',
|
||||
|
||||
@@ -6,10 +6,10 @@ import type { Category, Item } from './types';
|
||||
const VALID_CATEGORIES: Category[] = ['B', 'C', 'D'];
|
||||
|
||||
describe('classifyItem', () => {
|
||||
it('uses min dimensions 10×10×10 mm', () => {
|
||||
expect(DIMENSION_LIMITS.min).toEqual({ width: 10, depth: 10, height: 10 });
|
||||
it('uses min dimensions 10×10×2 mm', () => {
|
||||
expect(DIMENSION_LIMITS.min).toEqual({ width: 10, depth: 10, height: 2 });
|
||||
expect(DIMENSION_LIMITS.max).toEqual({ width: 450, depth: 320, height: 320 });
|
||||
expect(DIMENSION_LIMITS.roundnessThreshold).toBe(0.8);
|
||||
expect(DIMENSION_LIMITS.roundnessThreshold).toBe(0.7);
|
||||
});
|
||||
|
||||
it('routes normal box to B', () => {
|
||||
@@ -40,11 +40,11 @@ describe('classifyItem', () => {
|
||||
expect(classifyItem(getItem('SKU-010')).category).toBe('B');
|
||||
});
|
||||
|
||||
it('rejects height below 10 mm as C', () => {
|
||||
it('rejects height below 2 mm as C', () => {
|
||||
const item: Item = {
|
||||
...getItem('SKU-001'),
|
||||
id: 'TEST-LOW-HEIGHT',
|
||||
dimensionsMm: { width: 100, depth: 100, height: 9 },
|
||||
dimensionsMm: { width: 100, depth: 100, height: 1 },
|
||||
};
|
||||
expect(classifyItem(item).category).toBe('C');
|
||||
expect(classifyItem(item).dimensionsPass).toBe(false);
|
||||
|
||||
@@ -16,9 +16,9 @@ export const CATEGORY_DEFINITIONS: Record<Category, CategoryDefinition> = {
|
||||
};
|
||||
|
||||
export const DIMENSION_LIMITS = {
|
||||
min: { width: 10, depth: 10, height: 10 },
|
||||
min: { width: 10, depth: 10, height: 2 },
|
||||
max: { width: 450, depth: 320, height: 320 },
|
||||
roundnessThreshold: 0.8,
|
||||
roundnessThreshold: 0.7,
|
||||
};
|
||||
|
||||
export function classifyItem(item: Item): ClassificationResult {
|
||||
|
||||
105
src/styles.css
105
src/styles.css
@@ -690,6 +690,46 @@ button:disabled {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.proof-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
border: 2px solid var(--cyan);
|
||||
border-radius: 12px;
|
||||
background: rgba(56, 189, 248, 0.06);
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
color: var(--cyan);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.summary-row strong {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.summary-row em {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.proof-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -976,6 +1016,71 @@ button:disabled {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ---------- C-priority explanation ---------- */
|
||||
|
||||
.c-priority-section {
|
||||
margin: clamp(40px, 6vw, 72px) 0;
|
||||
}
|
||||
|
||||
.c-priority-card {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: clamp(20px, 4vw, 32px);
|
||||
border: 2px solid var(--orange);
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(135deg, rgba(245, 158, 11, 0.08) 0%, rgba(245, 158, 11, 0.02) 100%);
|
||||
}
|
||||
|
||||
.c-priority-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 16px;
|
||||
background: var(--orange);
|
||||
color: #041018;
|
||||
font-size: 36px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.c-priority-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.c-priority-content h3 {
|
||||
font-size: 1.5rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.c-priority-content p {
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.c-priority-content .btn-primary {
|
||||
align-self: flex-start;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.c-priority-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.c-priority-content .btn-primary {
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Engineering details ---------- */
|
||||
|
||||
.engineering-toggle {
|
||||
|
||||
Reference in New Issue
Block a user