feat: add guided demo view
This commit is contained in:
21
README.md
21
README.md
@@ -30,6 +30,27 @@ https://www.arhipovdan.ru/
|
||||
http://127.0.0.1:3100/
|
||||
```
|
||||
|
||||
### Guided Demo
|
||||
|
||||
Рекомендуемый путь для жюри — использовать **Guided Demo** view.
|
||||
В Header переключите режим на `Guided Demo`.
|
||||
Это чистое демо-представление для защиты (1 экран без скролла).
|
||||
|
||||
Отличия Guided Demo от Engineering Dashboard:
|
||||
- Guided Demo показывает только ключевую информацию для защиты (Proof Card, Narration, Snapshot), скрывая длинные логи и графики.
|
||||
- Engineering Mode дает полный доступ ко всем метрикам, логам, PID-графику и дереву классификации.
|
||||
|
||||
Рекомендуемый путь:
|
||||
- Start Guided Demo
|
||||
- Next
|
||||
- Oversized
|
||||
- Round
|
||||
- Low confidence
|
||||
- Jam
|
||||
- Emergency stop
|
||||
|
||||
В Guided Demo закрываются критерии: classification correctness, routing, timing, fault handling.
|
||||
|
||||
## Presentation Mode
|
||||
|
||||
В Header есть переключатель:
|
||||
|
||||
@@ -1,56 +1,28 @@
|
||||
# Demo Script
|
||||
|
||||
Open https://arhipovdan.ru/ and switch Header to `Presentation Mode`.
|
||||
Open https://arhipovdan.ru/ and switch Header to `Guided Demo`.
|
||||
|
||||
## 3-Minute Defense
|
||||
## 30-Second Pitch
|
||||
|
||||
### 0:00-0:25 — System Overview
|
||||
"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."
|
||||
|
||||
Demo step: `System overview`.
|
||||
## 3-Minute Guided Demo
|
||||
|
||||
Say: the system models the full loop: detection -> classification -> actuator command -> physical route.
|
||||
1. Open **Guided Demo**.
|
||||
2. Click **Start Guided Demo**.
|
||||
3. Point to the **Current Proof Card** and the **Scene**.
|
||||
- "Watch how detection leads to classification and routing."
|
||||
4. Click **Next** to show `Oversized item to C`.
|
||||
- "Dimensions fail first, priority C."
|
||||
5. Click **Next** to show `Round object to D`.
|
||||
- "Dimensions pass, but roundness triggers D."
|
||||
6. Click **Next** through `Boundary`, `Low confidence`, `Jam`, `Emergency stop`.
|
||||
- On faults: click "Apply fault scenario" to confirm.
|
||||
- "The line stops safely on emergency."
|
||||
|
||||
Watch: engineering SVG scene, sensors, stop-gate, pushers and B/C/D routes.
|
||||
## 5-Minute Guided Demo
|
||||
|
||||
### 0:25-0:55 — Normal Item To B
|
||||
|
||||
Demo step: `Normal item to B`.
|
||||
|
||||
Apply scenario, use `Run suggested action` or `Step state`.
|
||||
|
||||
Say: dimensions pass, roundness is below threshold, category B opens the gate and sends the item straight.
|
||||
|
||||
### 0:55-1:25 — Oversized To C
|
||||
|
||||
Demo step: `Oversized item to C`.
|
||||
|
||||
Say: dimensions fail first, so C has priority even before shape routing.
|
||||
|
||||
Watch: decision tree FAIL, `ROUTE_TO_C`, lower roll-cage.
|
||||
|
||||
### 1:25-1:55 — Round Object To D
|
||||
|
||||
Demo step: `Round object to D`.
|
||||
|
||||
Say: dimensions pass, but roundness >= 0.8, so the item goes to D for repack/shape issue.
|
||||
|
||||
### 1:55-2:25 — Fault And Safety
|
||||
|
||||
Demo step: `Jam / fault handling` or `Emergency stop`.
|
||||
|
||||
With Safe Demo ON, confirm the fault scenario intentionally.
|
||||
|
||||
Say: abnormal states stop conveyor motion and require Reset.
|
||||
|
||||
### 2:25-3:00 — Criteria Coverage
|
||||
|
||||
Demo step: `Performance and synchronization`, then focus criteria panel.
|
||||
|
||||
Say: this checklist maps every OZON criterion to scenario, component and doc evidence.
|
||||
|
||||
## 5-Minute Defense
|
||||
|
||||
Use all 10 demo steps:
|
||||
Use all 10 demo steps in Guided Demo View:
|
||||
|
||||
1. System overview.
|
||||
2. Normal item to B.
|
||||
@@ -65,11 +37,20 @@ Use all 10 demo steps:
|
||||
|
||||
For each step:
|
||||
|
||||
1. Read `What to watch`.
|
||||
2. Apply recommended scenario.
|
||||
3. Use `Step state` or `Run suggested action`.
|
||||
4. Point to the highlighted focus area.
|
||||
5. Close with the presenter phrase.
|
||||
1. Read the **Demo Narration Card**.
|
||||
2. Use **Next** or the **Primary Action Button**.
|
||||
3. Point to the highlighted values in the **Proof Card**.
|
||||
4. Read the **Presenter Phrase** from the screen.
|
||||
|
||||
## What to say on each step
|
||||
|
||||
Use the `presenterPhrase` text shown in the **Demo Narration Card**. It is specifically written to be short and clear for the jury.
|
||||
|
||||
## What to do if demo gets stuck
|
||||
|
||||
- Click **Reset Demo**.
|
||||
- If still stuck, switch back to **Engineering Mode** and use manual scenario selection.
|
||||
- Proceed manually.
|
||||
|
||||
## Fallback If Domain Does Not Open
|
||||
|
||||
|
||||
@@ -59,3 +59,19 @@ The stop-gate fixes the item. B opens the gate, C extends pusher C, D extends pu
|
||||
## 15. What Happens On Jam?
|
||||
|
||||
The system enters FAULT, conveyor target speed becomes 0, actual speed decays toward 0, and Reset is required.
|
||||
|
||||
## 16. Why is the interface divided into Guided and Engineering?
|
||||
|
||||
Guided Demo is focused solely on the defense presentation, showing only the Proof Card, Scene, and Narrative, ensuring clarity in a 3-minute pitch. Engineering Mode provides full access to logs, PID, and internal state.
|
||||
|
||||
## 17. How does Guided Demo prove engineering realism?
|
||||
|
||||
Even in Guided Demo, the scene and proof card are driven by the real underlying state machine, classifier, and metric engine—not a pre-rendered video.
|
||||
|
||||
## 18. Where to see the full event log and PID?
|
||||
|
||||
In the Engineering Dashboard.
|
||||
|
||||
## 19. Why aren't all panels shown in Guided Demo?
|
||||
|
||||
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.
|
||||
|
||||
@@ -11,6 +11,13 @@ Before final submission:
|
||||
- [ ] Presentation Mode opens.
|
||||
- [ ] Demo steps checked.
|
||||
- [ ] Safe Demo confirmation checked for jam/emergency.
|
||||
- [ ] Guided Demo verified on 1920x1080 (no scroll).
|
||||
- [ ] Guided Demo verified on 1440x900.
|
||||
- [ ] Start Guided Demo button works.
|
||||
- [ ] Next/Previous buttons work.
|
||||
- [ ] Reset Demo button works.
|
||||
- [ ] Fault confirmation works in Guided Demo.
|
||||
- [ ] Engineering Mode is not broken.
|
||||
- [ ] OZON Criteria Coverage Panel checked.
|
||||
- [ ] Engineering Mode still works.
|
||||
- [ ] Scenarios checked: normal_flow, oversized_item, round_object, boundary_dimensions, close_items, low_confidence, jam, emergency_stop.
|
||||
|
||||
29
src/App.tsx
29
src/App.tsx
@@ -5,6 +5,8 @@ import { SCENARIOS } from './data/scenarios';
|
||||
import { createSimulation, setRunning, stepSimulation, stepSimulationToNextState } from './domain/simulation';
|
||||
import type { DemoStep, PresentationMode, ScenarioId, SimulationState } from './domain/types';
|
||||
|
||||
import GuidedDemoView from './components/GuidedDemoView';
|
||||
|
||||
const TICK_MS = 250;
|
||||
|
||||
function isFaultDemoStep(step: DemoStep): boolean {
|
||||
@@ -13,7 +15,7 @@ function isFaultDemoStep(step: DemoStep): boolean {
|
||||
|
||||
export default function App() {
|
||||
const [activeScenarioId, setActiveScenarioId] = useState<ScenarioId>('normal_flow');
|
||||
const [presentationMode, setPresentationMode] = useState<PresentationMode>('engineering');
|
||||
const [presentationMode, setPresentationMode] = useState<PresentationMode>('guided');
|
||||
const [safeDemoEnabled, setSafeDemoEnabled] = useState(true);
|
||||
const [demoStepIndex, setDemoStepIndex] = useState(0);
|
||||
const [pendingFaultStepId, setPendingFaultStepId] = useState<string | undefined>();
|
||||
@@ -87,7 +89,30 @@ export default function App() {
|
||||
setActiveScenarioId(DEMO_STEPS[0].scenarioId);
|
||||
};
|
||||
|
||||
return (
|
||||
return presentationMode === 'guided' ? (
|
||||
<GuidedDemoView
|
||||
simulation={simulation}
|
||||
scenarios={SCENARIOS}
|
||||
activeScenarioId={activeScenarioId}
|
||||
presentationMode={presentationMode}
|
||||
safeDemoEnabled={safeDemoEnabled}
|
||||
demoSteps={DEMO_STEPS}
|
||||
demoStepIndex={demoStepIndex}
|
||||
currentDemoStep={currentDemoStep}
|
||||
pendingFaultStepId={pendingFaultStepId}
|
||||
onStart={handleStart}
|
||||
onPause={handlePause}
|
||||
onReset={handleReset}
|
||||
onStep={handleStep}
|
||||
onScenarioChange={handleScenarioChange}
|
||||
onPresentationModeChange={setPresentationMode}
|
||||
onSafeDemoChange={setSafeDemoEnabled}
|
||||
onDemoStepChange={handleDemoStepChange}
|
||||
onApplyDemoStepScenario={handleApplyDemoStepScenario}
|
||||
onRunPreferredAction={handleRunPreferredAction}
|
||||
onRestartDemo={handleRestartDemo}
|
||||
/>
|
||||
) : (
|
||||
<Dashboard
|
||||
simulation={simulation}
|
||||
scenarios={SCENARIOS}
|
||||
|
||||
98
src/components/CurrentProofCard.tsx
Normal file
98
src/components/CurrentProofCard.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { DIMENSION_LIMITS } from '../domain/classifier';
|
||||
import type { SimulationState } from '../domain/types';
|
||||
|
||||
export default function CurrentProofCard({ simulation }: { simulation: SimulationState }) {
|
||||
const current = simulation.currentItem;
|
||||
const result = current?.classification;
|
||||
const item = current?.item;
|
||||
const dimensions = item?.dimensionsMm;
|
||||
const lowConfidence = Boolean(item && item.confidence < 0.65);
|
||||
const stopped = simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
|
||||
|
||||
if (!current) {
|
||||
return (
|
||||
<section 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>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
<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>
|
||||
</div>
|
||||
<div className="proof-item">
|
||||
<span className="proof-label">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>}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
182
src/components/GuidedDemoView.tsx
Normal file
182
src/components/GuidedDemoView.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,8 @@ export default function Header({
|
||||
<div className="header-status">
|
||||
<div className="mode-toggle" role="group" aria-label="Dashboard mode">
|
||||
<button className={presentationMode === 'engineering' ? 'active' : ''} onClick={() => onPresentationModeChange('engineering')}>Engineering Mode</button>
|
||||
<button className={presentationMode === 'presentation' ? 'active' : ''} onClick={() => onPresentationModeChange('presentation')}>Presentation Mode</button>
|
||||
<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)}>
|
||||
|
||||
@@ -188,7 +188,7 @@ export interface SimulationState {
|
||||
}
|
||||
|
||||
|
||||
export type PresentationMode = 'engineering' | 'presentation';
|
||||
export type PresentationMode = 'engineering' | 'presentation' | 'guided';
|
||||
export type DemoPreferredAction = 'start' | 'step' | 'reset' | 'pause';
|
||||
export type DemoFocusArea = 'scene' | 'classification' | 'timeline' | 'pid' | 'eventLog' | 'criteria' | 'safety';
|
||||
export type CriteriaStatus = 'covered' | 'partially covered' | 'demo step available';
|
||||
|
||||
300
src/styles.css
300
src/styles.css
@@ -1123,3 +1123,303 @@ button:disabled {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Guided Demo View */
|
||||
.guided-demo-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.guided-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 20px;
|
||||
background: rgba(7, 14, 23, 0.95);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guided-header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.guided-header-center {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.guided-header-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
background: var(--cyan);
|
||||
color: #000;
|
||||
border-color: var(--cyan);
|
||||
font-size: 14px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.primary-action:hover {
|
||||
background: #7dd3fc;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.guided-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 420px;
|
||||
gap: 16px;
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.guided-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.guided-scene-panel {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.guided-scene-panel .scene-wrap {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.guided-scene-panel svg {
|
||||
flex-grow: 1;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.guided-metrics-strip {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guided-metric {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guided-metric span {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.guided-metric strong {
|
||||
font-size: 18px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.guided-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* Proof Card */
|
||||
.proof-card {
|
||||
padding: 16px;
|
||||
background: #081422;
|
||||
border: 1px solid var(--cyan);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 20px rgba(56, 189, 248, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.proof-card-stopped {
|
||||
border-color: var(--red);
|
||||
box-shadow: 0 0 20px rgba(251, 61, 78, 0.15);
|
||||
}
|
||||
|
||||
.proof-card-empty {
|
||||
border-color: #243e57;
|
||||
box-shadow: none;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.proof-card-empty h2 {
|
||||
font-size: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.proof-card-empty p {
|
||||
color: var(--muted);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.proof-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.proof-item {
|
||||
background: #0d1b2a;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #1a2f44;
|
||||
}
|
||||
|
||||
.proof-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.proof-item strong {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fail-text { color: var(--red); }
|
||||
.warn-text { color: var(--orange); }
|
||||
|
||||
.proof-reason {
|
||||
background: #0d1b2a;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #1a2f44;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.proof-reason .warning-note { margin-top: 8px; font-weight: bold; }
|
||||
.proof-reason .error-note { margin-top: 8px; color: var(--red); font-weight: bold; }
|
||||
|
||||
.evidence-chain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
background: #050a10;
|
||||
padding: 10px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #1e3a54;
|
||||
}
|
||||
|
||||
.evidence-chain span { color: #475569; }
|
||||
.evidence-chain .arrow { color: #334155; }
|
||||
.evidence-chain .done-step { color: var(--green); }
|
||||
.evidence-chain .active-step { color: var(--cyan); text-shadow: 0 0 8px rgba(56, 189, 248, 0.5); }
|
||||
|
||||
/* Narration */
|
||||
.guided-narration {
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guided-narration .explanation {
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.narration-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
background: #0d1b2a;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #1a2f44;
|
||||
}
|
||||
|
||||
.narration-details strong { color: var(--cyan); }
|
||||
|
||||
.presenter-phrase {
|
||||
margin: 0;
|
||||
padding: 14px;
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
border-left: 4px solid var(--cyan);
|
||||
border-radius: 0 10px 10px 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.fault-confirm-btn {
|
||||
margin-top: 12px;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid var(--orange);
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.fault-confirm-btn.pending {
|
||||
background: var(--orange);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* Criteria Snapshot */
|
||||
.guided-criteria {
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.criteria-list-mini {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.criterion-mini {
|
||||
background: #0d1b2a;
|
||||
border: 1px solid #1a2f44;
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.criterion-mini-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.criterion-mini-header strong { font-size: 13px; }
|
||||
.criterion-mini p { font-size: 12px; color: var(--muted); margin: 0; line-height: 1.4; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user