design: redesign product demo UI

This commit is contained in:
root
2026-07-04 01:44:32 +02:00
parent 2d50cdb454
commit cb38cd0002
16 changed files with 1162 additions and 721 deletions

View File

@@ -0,0 +1,29 @@
import { OZON_CRITERIA } from '../data/criteria';
export default function CriteriaCards() {
return (
<section className="criteria-cards-section" id="criteria">
<div className="section-header">
<h2>Покрытие критериев OZON</h2>
<p>Каждый инженерный критерий хакатона покрывается симуляцией и тестами.</p>
</div>
<div className="criteria-grid">
{OZON_CRITERIA.map((criterion) => (
<div key={criterion.id} className="criteria-card">
<div className="criteria-card-header">
<h3>{criterion.title}</h3>
<span className={`status-badge ${criterion.status.replace(/ /g, '-')}`}>
{criterion.status}
</span>
</div>
<p className="criteria-evidence">{criterion.evidence}</p>
<div className="criteria-footer">
<span className="related-docs">Docs: {criterion.relatedDoc}</span>
</div>
</div>
))}
</div>
</section>
);
}

View File

@@ -11,88 +11,60 @@ export default function CurrentProofCard({ simulation }: { simulation: Simulatio
if (!current) {
return (
<section className="proof-card proof-card-empty">
<div className="proof-card proof-card-empty">
<div className="proof-card-inner">
<p className="eyebrow">Awaiting cycle</p>
<h2>Ready to demonstrate sorting cycle</h2>
<p>The demo will show detection, classification, control command and routing to B/C/D.</p>
<div className="evidence-chain empty-chain">
<span>Detection</span>
<span className="arrow">→</span>
<span>Classification</span>
<span className="arrow">→</span>
<span>Command</span>
<span className="arrow">→</span>
<span>Actuator</span>
<span className="arrow">→</span>
<span>Route</span>
<span className="arrow">→</span>
<span>Log</span>
</div>
<p className="start-prompt">Click <strong>Start Guided Demo</strong> or Next to begin.</p>
<h3>Ready to demonstrate sorting cycle</h3>
<p>Нажмите «Запустить демо» для начала или переключите сценарий ниже.</p>
</div>
</section>
</div>
);
}
return (
<section className={`proof-card ${stopped ? 'proof-card-stopped' : ''}`}>
<div className="panel-heading">
<p className="eyebrow">Current Item Proof</p>
<h2>{item?.name} (ID: {item?.id})</h2>
<div className={`proof-card ${stopped ? 'proof-card-stopped' : ''}`}>
<div className="proof-card-header">
<p className="eyebrow">Результат классификации</p>
<h3>{item?.name} (ID: {item?.id})</h3>
</div>
<div className="proof-grid">
<div className="proof-item">
<span className="proof-label">Dimensions</span>
<strong className={result?.dimensionsPass === false ? 'fail-text' : ''}>
{dimensions?.width} x {dimensions?.depth} x {dimensions?.height} mm
</strong>
<span className="proof-label">Категория</span>
<strong className={`category-${result?.category || 'default'}`}>{result?.category || '-'}</strong>
</div>
<div className="proof-item">
<span className="proof-label">Roundness</span>
<span className="proof-label">Маршрут</span>
<strong>{result?.category ? `Зона ${result.category}` : '-'}</strong>
</div>
<div className="proof-item">
<span className="proof-label">Команда</span>
<strong>{simulation.machineState.startsWith('ROUTE_TO_') ? simulation.machineState : 'WAITING'}</strong>
</div>
</div>
<div className="proof-details">
<div className="detail-row">
<span>Габариты:</span>
<strong className={result?.dimensionsPass === false ? 'fail-text' : ''}>
{dimensions?.width} x {dimensions?.depth} x {dimensions?.height} мм
</strong>
</div>
<div className="detail-row">
<span>Roundness:</span>
<strong className={result?.roundnessPass === false && result?.dimensionsPass ? 'fail-text' : ''}>
{item?.roundness.toFixed(2)}
</strong>
</div>
<div className="proof-item">
<span className="proof-label">Confidence</span>
<strong className={lowConfidence ? 'warn-text' : ''}>
{(item?.confidence ?? 0 * 100).toFixed(0)}%
</strong>
</div>
<div className="proof-item">
<span className="proof-label">Category</span>
<strong className={`category-${result?.category}`}>{result?.category}</strong>
</div>
<div className="proof-item">
<span className="proof-label">Command</span>
<strong>{simulation.machineState.startsWith('ROUTE_TO_') ? simulation.machineState : 'WAITING'}</strong>
</div>
<div className="proof-item">
<span className="proof-label">Target zone</span>
<strong>{result?.category ? `Zone ${result.category}` : '-'}</strong>
</div>
</div>
<div className="proof-reason">
<strong>Decision reason:</strong> {result?.reason}
{lowConfidence && <p className="warning-note">Low CV confidence — rule-based fallback used</p>}
{simulation.machineState === 'FAULT' && <p className="error-note">FAULT: Conveyor stopped, reset required</p>}
{simulation.machineState === 'EMERGENCY_STOP' && <p className="error-note">EMERGENCY_STOP: All actuators stopped</p>}
{simulation.metrics.queueLength > 1 && <p className="warning-note">Spacing warning: sequential processing enforced</p>}
<strong>Почему такое решение:</strong> {result?.reason}
{lowConfidence && <p className="warning-note">Низкая уверенность CV — использован rule-based fallback</p>}
{simulation.machineState === 'FAULT' && <p className="error-note">FAULT: Конвейер остановлен</p>}
{simulation.machineState === 'EMERGENCY_STOP' && <p className="error-note">EMERGENCY_STOP: Аварийная остановка</p>}
{simulation.metrics.queueLength > 1 && <p className="warning-note">Внимание: очередь товаров</p>}
</div>
<div className="evidence-chain active-chain">
<span className={simulation.machineState === 'DETECTING' ? 'active-step' : 'done-step'}>Detection</span>
<span className="arrow">→</span>
<span className={simulation.machineState === 'CLASSIFYING' ? 'active-step' : (result ? 'done-step' : '')}>Classification</span>
<span className="arrow">→</span>
<span className={simulation.machineState.startsWith('ROUTE_TO_') ? 'active-step' : (result ? 'done-step' : '')}>Command</span>
<span className="arrow">→</span>
<span className={simulation.actuators.pusherC !== 'idle' || simulation.actuators.pusherD !== 'idle' ? 'active-step' : (result ? 'done-step' : '')}>Actuator</span>
<span className="arrow">→</span>
<span className={simulation.machineState.startsWith('ROUTE_TO_') ? 'active-step' : ''}>Route</span>
<span className="arrow">→</span>
<span className={simulation.machineState === 'RETURN_HOME' ? 'active-step' : ''}>Log</span>
</div>
</section>
</div>
);
}

View File

@@ -1,128 +0,0 @@
import Header from './Header';
import SorterScene from './SorterScene';
import CurrentItemPanel from './CurrentItemPanel';
import ClassificationPanel from './ClassificationPanel';
import StateMachinePanel from './StateMachinePanel';
import MetricsPanel from './MetricsPanel';
import EventLog from './EventLog';
import ScenarioPanel from './ScenarioPanel';
import SensorPanel from './SensorPanel';
import PidPanel from './PidPanel';
import TimelinePanel from './TimelinePanel';
import PresentationModePanel from './PresentationModePanel';
import OzonCriteriaPanel from './OzonCriteriaPanel';
import type { DemoFocusArea, DemoStep, PresentationMode, Scenario, ScenarioId, SimulationState } from '../domain/types';
interface DashboardProps {
simulation: SimulationState;
scenarios: Scenario[];
activeScenarioId: ScenarioId;
presentationMode: PresentationMode;
safeDemoEnabled: boolean;
demoSteps: DemoStep[];
demoStepIndex: number;
currentDemoStep: DemoStep;
pendingFaultStepId?: string;
onStart: () => void;
onPause: () => void;
onReset: () => void;
onStep: () => void;
onScenarioChange: (scenarioId: ScenarioId) => void;
onPresentationModeChange: (mode: PresentationMode) => void;
onSafeDemoChange: (enabled: boolean) => void;
onDemoStepChange: (index: number) => void;
onApplyDemoStepScenario: (step: DemoStep) => void;
onRunPreferredAction: (step: DemoStep) => void;
onRestartDemo: () => void;
}
function focusClass(mode: PresentationMode, activeFocus: DemoFocusArea, panelFocus: DemoFocusArea | DemoFocusArea[]): string {
if (mode !== 'presentation') {
return '';
}
const focusList = Array.isArray(panelFocus) ? panelFocus : [panelFocus];
return focusList.includes(activeFocus) || (activeFocus === 'safety' && focusList.includes('scene')) ? 'focus-highlight' : 'presentation-muted';
}
export default function Dashboard({
simulation,
scenarios,
activeScenarioId,
presentationMode,
safeDemoEnabled,
demoSteps,
demoStepIndex,
currentDemoStep,
pendingFaultStepId,
onStart,
onPause,
onReset,
onStep,
onScenarioChange,
onPresentationModeChange,
onSafeDemoChange,
onDemoStepChange,
onApplyDemoStepScenario,
onRunPreferredAction,
onRestartDemo,
}: DashboardProps) {
const focusArea = currentDemoStep.focusArea;
return (
<div className={`app-shell mode-${presentationMode}`}>
<Header
simulation={simulation}
presentationMode={presentationMode}
safeDemoEnabled={safeDemoEnabled}
onStart={onStart}
onPause={onPause}
onReset={onReset}
onStep={onStep}
onPresentationModeChange={onPresentationModeChange}
onSafeDemoChange={onSafeDemoChange}
/>
{presentationMode === 'presentation' ? (
<PresentationModePanel
steps={demoSteps}
currentStep={currentDemoStep}
currentIndex={demoStepIndex}
activeScenarioId={activeScenarioId}
safeDemoEnabled={safeDemoEnabled}
pendingFaultStepId={pendingFaultStepId}
onPrevious={() => onDemoStepChange(demoStepIndex - 1)}
onNext={() => onDemoStepChange(demoStepIndex + 1)}
onRestart={onRestartDemo}
onApplyScenario={() => onApplyDemoStepScenario(currentDemoStep)}
onRunPreferredAction={() => onRunPreferredAction(currentDemoStep)}
/>
) : null}
<main className="dashboard-grid">
<section className={`scene-column panel panel-scene ${focusClass(presentationMode, focusArea, ['scene', 'safety'])}`}>
<SorterScene simulation={simulation} />
</section>
<aside className="right-rail">
<div className={focusClass(presentationMode, focusArea, 'classification')}><CurrentItemPanel simulation={simulation} /></div>
<div className={focusClass(presentationMode, focusArea, 'classification')}><ClassificationPanel simulation={simulation} /></div>
<div className={focusClass(presentationMode, focusArea, 'timeline')}><StateMachinePanel currentState={simulation.machineState} /></div>
<div className={focusClass(presentationMode, focusArea, 'scene')}><SensorPanel simulation={simulation} /></div>
</aside>
<section className="bottom-row">
<div className={focusClass(presentationMode, focusArea, 'timeline')}><MetricsPanel metrics={simulation.metrics} /></div>
<div className={focusClass(presentationMode, focusArea, 'timeline')}><TimelinePanel simulation={simulation} /></div>
<div className={focusClass(presentationMode, focusArea, ['pid', 'safety'])}><PidPanel pid={simulation.pid} metrics={simulation.metrics} status={simulation.systemStatus} /></div>
<div className={focusClass(presentationMode, focusArea, 'eventLog')}><EventLog events={simulation.events} /></div>
<div className={focusClass(presentationMode, focusArea, 'criteria')}>
<OzonCriteriaPanel currentStep={currentDemoStep} />
</div>
<div className="scenario-panel-wrap">
<ScenarioPanel
scenarios={scenarios}
activeScenarioId={activeScenarioId}
onScenarioChange={onScenarioChange}
/>
</div>
</section>
</main>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { useState } from 'react';
import StateMachinePanel from './StateMachinePanel';
import TimelinePanel from './TimelinePanel';
import PidPanel from './PidPanel';
import EventLog from './EventLog';
import MetricsPanel from './MetricsPanel';
import SensorPanel from './SensorPanel';
import type { SimulationState } from '../domain/types';
export default function EngineeringDetails({ simulation }: { simulation: SimulationState }) {
const [isOpen, setIsOpen] = useState(false);
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>
);
}

View File

@@ -1,182 +0,0 @@
import { useEffect } from 'react';
import SorterScene from './SorterScene';
import CurrentProofCard from './CurrentProofCard';
import { OZON_CRITERIA } from '../data/criteria';
import type { DemoStep, Scenario, ScenarioId, SimulationState, PresentationMode } from '../domain/types';
interface GuidedDemoViewProps {
simulation: SimulationState;
scenarios: Scenario[];
activeScenarioId: ScenarioId;
presentationMode: PresentationMode;
safeDemoEnabled: boolean;
demoSteps: DemoStep[];
demoStepIndex: number;
currentDemoStep: DemoStep;
pendingFaultStepId?: string;
onStart: () => void;
onPause: () => void;
onReset: () => void;
onStep: () => void;
onScenarioChange: (scenarioId: ScenarioId) => void;
onPresentationModeChange: (mode: any) => void;
onSafeDemoChange: (enabled: boolean) => void;
onDemoStepChange: (index: number) => void;
onApplyDemoStepScenario: (step: DemoStep) => void;
onRunPreferredAction: (step: DemoStep) => void;
onRestartDemo: () => void;
}
export default function GuidedDemoView({
simulation,
scenarios,
activeScenarioId,
demoSteps,
demoStepIndex,
currentDemoStep,
pendingFaultStepId,
safeDemoEnabled,
onStart,
onPause,
onReset,
onStep,
onPresentationModeChange,
onDemoStepChange,
onApplyDemoStepScenario,
onRunPreferredAction,
onRestartDemo,
}: GuidedDemoViewProps) {
const isFaultStep = currentDemoStep.scenarioId === 'jam' || currentDemoStep.scenarioId === 'emergency_stop';
const pendingFaultConfirmation = pendingFaultStepId === currentDemoStep.id;
const handleStartDemo = () => {
if (!simulation.currentItem && demoStepIndex === 0 && !simulation.running) {
onRunPreferredAction(currentDemoStep);
} else if (demoStepIndex === 0 && simulation.running) {
onRestartDemo();
} else {
onRunPreferredAction(currentDemoStep);
}
};
const handleNext = () => {
onDemoStepChange(demoStepIndex + 1);
};
const handlePrevious = () => {
onDemoStepChange(demoStepIndex - 1);
};
const relatedCriteriaList = OZON_CRITERIA.filter(c => currentDemoStep.relatedCriteria.includes(c.id));
return (
<div className="guided-demo-layout">
{/* 1. Top Demo Header */}
<header className="guided-header">
<div className="guided-header-left">
<p className="eyebrow">Guided Sorting Demo</p>
<h1>Step {demoStepIndex + 1} / {demoSteps.length}: {currentDemoStep.title}</h1>
</div>
<div className="guided-header-center">
<div className={`status-pill status-${simulation.systemStatus.toLowerCase()}`}>
<span />
{simulation.systemStatus}
</div>
<div className="scenario-label">Scenario: {simulation.scenario.name}</div>
</div>
<div className="guided-header-right">
<button className="primary-action" onClick={handleStartDemo}>
{simulation.currentItem ? 'Restart Guided Demo' : 'Start Guided Demo'}
</button>
<div className="nav-actions">
<button onClick={handlePrevious} disabled={demoStepIndex === 0}>Previous</button>
<button onClick={handleNext} disabled={demoStepIndex === demoSteps.length - 1}>Next</button>
</div>
<button onClick={onReset}>Reset</button>
<button onClick={() => onPresentationModeChange('engineering')}>Back to Engineering</button>
</div>
</header>
{/* Main Grid */}
<main className="guided-grid">
{/* 2. Large Scene Area */}
<section className="guided-left">
<div className="panel panel-scene guided-scene-panel">
<SorterScene simulation={simulation} />
<div className="guided-metrics-strip">
<div className="guided-metric">
<span>Cycle Time</span>
<strong>{simulation.metrics.avgCycleTimeMs} ms</strong>
</div>
<div className="guided-metric">
<span>CV Latency</span>
<strong>{simulation.metrics.cvLatencyMs} ms</strong>
</div>
<div className="guided-metric">
<span>Actuator Latency</span>
<strong>{simulation.metrics.actuatorLatencyMs} ms</strong>
</div>
<div className="guided-metric">
<span>Throughput</span>
<strong>{simulation.metrics.throughputItemsPerMin} items/min</strong>
</div>
</div>
</div>
</section>
{/* Right Column */}
<aside className="guided-right">
{/* 3. Current Proof Card */}
<CurrentProofCard simulation={simulation} />
{/* 4. Demo Narration Card */}
<section className="panel guided-narration">
<div className="panel-heading">
<p className="eyebrow">Demo Narration</p>
<h2>{currentDemoStep.title}</h2>
</div>
<p className="explanation">{currentDemoStep.explanation}</p>
<div className="narration-details">
<div>
<strong>Watch:</strong> {currentDemoStep.whatToWatch}
</div>
<div>
<strong>Jury Value:</strong> {currentDemoStep.juryValue}
</div>
</div>
<blockquote className="presenter-phrase">
"{currentDemoStep.presenterPhrase}"
</blockquote>
{isFaultStep && safeDemoEnabled && (
<button
className={`fault-confirm-btn ${pendingFaultConfirmation ? 'pending' : ''}`}
onClick={() => onApplyDemoStepScenario(currentDemoStep)}
>
{pendingFaultConfirmation ? 'Click again to confirm fault scenario' : 'Apply fault scenario'}
</button>
)}
</section>
{/* 5. Criteria Snapshot */}
<section className="panel guided-criteria">
<div className="panel-heading">
<p className="eyebrow">Criteria Snapshot</p>
<h2>This step covers</h2>
</div>
<div className="criteria-list-mini">
{relatedCriteriaList.map(c => (
<div key={c.id} className="criterion-mini">
<div className="criterion-mini-header">
<strong>{c.title}</strong>
<span className={`criterion-status status-${c.status.replace(/ /g, '-')}`}>{c.status}</span>
</div>
<p>{c.evidence}</p>
</div>
))}
</div>
</section>
</aside>
</main>
</div>
);
}

View File

@@ -1,59 +1,31 @@
import type { PresentationMode, SimulationState } from '../domain/types';
import type { SimulationState } from '../domain/types';
interface HeaderProps {
simulation: SimulationState;
presentationMode: PresentationMode;
safeDemoEnabled: boolean;
onStart: () => void;
onPause: () => void;
onReset: () => void;
onStep: () => void;
onPresentationModeChange: (mode: PresentationMode) => void;
onSafeDemoChange: (enabled: boolean) => void;
}
export default function Header({
simulation,
presentationMode,
safeDemoEnabled,
onStart,
onPause,
onReset,
onStep,
onPresentationModeChange,
onSafeDemoChange,
}: HeaderProps) {
const locked = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
export default function Header({ simulation }: HeaderProps) {
return (
<header className="header">
<div>
<p className="eyebrow">Intelligent robotic sorting line</p>
<h1>OZON Tech Sorter Simulation</h1>
<header className="site-header">
<div className="header-logo">
<span className="logo-icon">O</span>
<div className="logo-text">
<span className="logo-title">OZON Tech</span>
<span className="logo-subtitle">Sorter Simulation</span>
</div>
</div>
<div className="header-status">
<div className="mode-toggle" role="group" aria-label="Dashboard mode">
<button className={presentationMode === 'engineering' ? 'active' : ''} onClick={() => onPresentationModeChange('engineering')}>Engineering Mode</button>
<button className={presentationMode === 'presentation' ? 'active' : ''} onClick={() => onPresentationModeChange('presentation')}>Presentation Dashboard</button>
<button className={presentationMode === 'guided' ? 'active' : ''} onClick={() => onPresentationModeChange('guided')}>Guided Demo</button>
</div>
{presentationMode === 'presentation' ? (
<button className={safeDemoEnabled ? 'safe-demo-toggle active' : 'safe-demo-toggle'} onClick={() => onSafeDemoChange(!safeDemoEnabled)}>
Safe Demo: {safeDemoEnabled ? 'ON' : 'OFF'}
</button>
) : null}
<div className={`status-pill status-${simulation.systemStatus.toLowerCase()}`}>
<span />
{simulation.systemStatus}
</div>
<div className="scenario-label">Scenario: {simulation.scenario.name}</div>
<div className="scenario-label">Sim t+{(simulation.simTimeMs / 1000).toFixed(1)}s</div>
<div className="header-actions">
<button onClick={onStart} disabled={locked}>Start</button>
<button onClick={onPause}>Pause</button>
<button onClick={onReset}>Reset</button>
<button onClick={onStep} disabled={locked}>Step state</button>
</div>
<div className="header-nav">
<a href="#demo" className="nav-link">Демо</a>
<a href="#scenarios" className="nav-link">Сценарии</a>
<a href="#criteria" className="nav-link">Критерии</a>
<a href="#engineering" className="nav-link">Инженерный режим</a>
</div>
<div className="header-status-compact">
<div className={`status-dot ${simulation.systemStatus.toLowerCase()}`}></div>
<span>{simulation.systemStatus}</span>
<span className="sim-time">t+{(simulation.simTimeMs / 1000).toFixed(1)}s</span>
</div>
</header>
);

View File

@@ -0,0 +1,51 @@
export default function HeroSection({ onStartDemo }: { onStartDemo: () => void }) {
return (
<section className="hero-section">
<div className="hero-content">
<h1 className="hero-title">Интеллектуальная система сортировки товаров</h1>
<p className="hero-subtitle">
Симуляция показывает полный цикл: обнаружение товара, классификация по правилам,
управляющая команда и маршрутизация в зоны B, C, D.
</p>
<div className="hero-badges">
<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>
<div className="hero-actions">
<button className="btn-primary" onClick={onStartDemo}>Запустить демо</button>
<a href="#scenarios" className="btn-secondary">Показать сценарии</a>
<a href="#engineering" className="btn-secondary">Инженерный режим</a>
</div>
</div>
<div className="hero-visual">
<div className="hero-chain">
<div className="chain-step">
<div className="chain-icon">📷</div>
<span>Detection</span>
</div>
<div className="chain-arrow">→</div>
<div className="chain-step">
<div className="chain-icon">🧠</div>
<span>Classification</span>
</div>
<div className="chain-arrow">→</div>
<div className="chain-step">
<div className="chain-icon">⚡</div>
<span>Command</span>
</div>
<div className="chain-arrow">→</div>
<div className="chain-step">
<div className="chain-icon">📦</div>
<span>Routing</span>
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,80 +0,0 @@
import type { DemoStep, ScenarioId } from '../domain/types';
interface PresentationModePanelProps {
steps: DemoStep[];
currentStep: DemoStep;
currentIndex: number;
activeScenarioId: ScenarioId;
safeDemoEnabled: boolean;
pendingFaultStepId?: string;
onPrevious: () => void;
onNext: () => void;
onRestart: () => void;
onApplyScenario: () => void;
onRunPreferredAction: () => void;
}
export default function PresentationModePanel({
steps,
currentStep,
currentIndex,
activeScenarioId,
safeDemoEnabled,
pendingFaultStepId,
onPrevious,
onNext,
onRestart,
onApplyScenario,
onRunPreferredAction,
}: PresentationModePanelProps) {
const scenarioApplied = activeScenarioId === currentStep.scenarioId;
const pendingFaultConfirmation = pendingFaultStepId === currentStep.id;
const isFaultStep = currentStep.scenarioId === 'jam' || currentStep.scenarioId === 'emergency_stop';
return (
<section className="presentation-panel panel">
<div className="presentation-step-index">Step {currentIndex + 1} / {steps.length}</div>
<div className="presentation-copy">
<p className="eyebrow">Presentation Mode</p>
<h2>{currentStep.title}</h2>
<p>{currentStep.explanation}</p>
</div>
<div className="presentation-proof">
<div>
<strong>What to watch</strong>
<span>{currentStep.whatToWatch}</span>
</div>
<div>
<strong>What it proves for jury</strong>
<span>{currentStep.juryValue}</span>
</div>
<div>
<strong>Current proof</strong>
<span>classification decision {'->'} control command {'->'} physical route {'->'} logged result</span>
</div>
</div>
<blockquote>{currentStep.presenterPhrase}</blockquote>
<div className="presentation-meta">
<span>Recommended scenario: <strong>{currentStep.scenarioId}</strong></span>
<span>Action: <strong>{currentStep.preferredAction}</strong></span>
<span>Focus: <strong>{currentStep.focusArea}</strong></span>
<span>Safe Demo: <strong>{safeDemoEnabled ? 'ON' : 'OFF'}</strong></span>
</div>
<div className="criteria-tags">
{currentStep.relatedCriteria.map((criterion) => <span key={criterion}>{criterion}</span>)}
</div>
{safeDemoEnabled && isFaultStep && pendingFaultConfirmation ? (
<p className="confirm-note">Click “Confirm fault scenario” again to intentionally switch to {currentStep.scenarioId}.</p>
) : null}
<div className="presentation-actions">
<button onClick={onPrevious} disabled={currentIndex === 0}>Previous step</button>
<button onClick={onNext} disabled={currentIndex === steps.length - 1}>Next step</button>
<button onClick={onRestart}>Restart demo</button>
<button className={scenarioApplied ? 'active' : ''} onClick={onApplyScenario}>
{pendingFaultConfirmation ? 'Confirm fault scenario' : scenarioApplied ? 'Scenario applied' : 'Apply scenario'}
</button>
<button onClick={onRunPreferredAction}>{scenarioApplied ? 'Run suggested action' : 'Apply then run'}</button>
</div>
</section>
);
}

View File

@@ -0,0 +1,41 @@
import SorterScene from './SorterScene';
import CurrentProofCard from './CurrentProofCard';
import StorylineStepper from './StorylineStepper';
import type { SimulationState } from '../domain/types';
interface Props {
simulation: SimulationState;
onNext: () => void;
onReset: () => void;
demoStepTitle: string;
}
export default function ProductDemoSection({ simulation, onNext, onReset, demoStepTitle }: Props) {
return (
<section className="product-demo-section" id="demo">
<div className="demo-header">
<h2>Live Demo</h2>
<div className="demo-status">
Текущий шаг: <strong>{demoStepTitle}</strong>
<span className={`status-dot ${simulation.systemStatus.toLowerCase()}`}></span>
</div>
</div>
<StorylineStepper simulation={simulation} />
<div className="demo-layout">
<div className="demo-scene-container">
<SorterScene simulation={simulation} />
</div>
<div className="demo-proof-container">
<CurrentProofCard simulation={simulation} />
<div className="demo-controls">
<button className="btn-primary" onClick={onNext}>Следующий шаг (Next)</button>
<button className="btn-secondary" onClick={onReset}>Сброс (Reset)</button>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,45 @@
import type { Scenario, ScenarioId } from '../domain/types';
interface Props {
scenarios: Scenario[];
activeScenarioId: ScenarioId;
onScenarioChange: (id: ScenarioId) => void;
}
export default function ScenarioCards({ scenarios, activeScenarioId, onScenarioChange }: Props) {
return (
<section className="scenario-cards-section" id="scenarios">
<div className="section-header">
<h2>Сценарии тестирования (Jury Test Cases)</h2>
<p>Выберите сценарий для проверки поведения системы в разных условиях.</p>
</div>
<div className="scenario-grid">
{scenarios.map((scenario) => (
<div
key={scenario.id}
className={`scenario-card ${scenario.id === activeScenarioId ? 'active' : ''}`}
onClick={() => onScenarioChange(scenario.id)}
>
<div className="scenario-card-header">
<h3>{scenario.name}</h3>
{scenario.id === activeScenarioId && <span className="active-badge">Выбран</span>}
</div>
<p className="scenario-desc">{scenario.description}</p>
<div className="scenario-meta">
<div className="meta-item">
<span className="meta-label">Цель:</span>
<span className="meta-value">{scenario.goal}</span>
</div>
<div className="meta-item">
<span className="meta-label">Результат:</span>
<span className="meta-value">{scenario.expectedCategorySummary}</span>
</div>
</div>
<button className="btn-secondary btn-small mt-auto">Применить</button>
</div>
))}
</div>
</section>
);
}

View File

@@ -0,0 +1,65 @@
import type { SimulationState } from '../domain/types';
export default function StorylineStepper({ simulation }: { simulation: SimulationState }) {
const state = 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 (
<div className="storyline-stepper">
<Step
label="1. Detection"
desc="Распознавание габаритов"
active={isDetection}
done={Boolean(item) && !isDetection}
/>
<Step
label="2. Classification"
desc="Дерево решений"
active={isClassification}
done={Boolean(item?.classification) && !isClassification && !isDetection}
/>
<Step
label="3. Decision"
desc="Выбор категории"
active={isClassification && state === 'CLASSIFYING'}
done={Boolean(item?.classification)}
/>
<Step
label="4. Command"
desc="Сигнал механизмам"
active={isCommand}
done={isRouting}
/>
<Step
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>
);
}