diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 2804c6f..42dcd27 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -9,6 +9,8 @@ import { WelcomeScreen } from './components/WelcomeScreen' import { NewBlueprintWizard } from './components/NewBlueprintWizard' import { SettingsDialog } from './components/SettingsDialog' import { SimulationPanel } from './components/SimulationPanel' +import { getScenarios } from './data/template-scenarios' +import type { TemplateScenario } from './data/template-scenarios' import { UnsavedChangesGuard } from './components/UnsavedChangesGuard' import { useFileManager } from './hooks/useFileManager' import { useProjectDirectory } from './hooks/useProjectDirectory' @@ -25,6 +27,7 @@ export function App() { const [showSimPanel, setShowSimPanel] = useState(false) const [error, setError] = useState(null) const [rulesDataMap, setRulesDataMap] = useState({}) + const [simulationRules, setSimulationRules] = useState({}) const settingsHook = useSettings() const { settings } = settingsHook @@ -35,7 +38,17 @@ export function App() { codeSearchUrl: settings.codeSearchUrl || undefined, }) - const simulation = useSimulation(doc, rulesDataMap) + // Merge project rules with simulation-scoped rules for the simulation hook + const effectiveRules: RulesDataMap = { ...rulesDataMap, ...simulationRules } + const simulation = useSimulation(doc, effectiveRules) + const scenarios = doc ? getScenarios(doc.name) : [] + + const handleSelectScenario = useCallback( + (scenario: TemplateScenario | null) => { + setSimulationRules(scenario?.rulesData ?? {}) + }, + [], + ) // Review #5: gate on doc !== null only, not on rules presence const canSimulate = doc !== null @@ -44,6 +57,7 @@ export function App() { if (simulation.isActive) { simulation.stop() setShowSimPanel(false) + setSimulationRules({}) } else { setShowSimPanel(true) } @@ -135,6 +149,7 @@ export function App() { setDoc(null) fileManager.setDirty(false) setRulesDataMap({}) + setSimulationRules({}) setError(null) }, [fileManager, simulation]) @@ -229,8 +244,10 @@ export function App() { onChange={handleChange} theme={settings.theme} symbolSearch={symbolSearch ?? undefined} - rulesDataMap={rulesDataMap} + rulesDataMap={effectiveRules} nodeHighlights={simulation.nodeHighlights} + edgeHighlights={simulation.edgeHighlights} + simulationAnimation={simulation.simulationAnimation} showYamlPreview showExportButton style={{ width: '100%', height: '100%' }} @@ -243,8 +260,11 @@ export function App() { stop: () => { simulation.stop() setShowSimPanel(false) + setSimulationRules({}) }, }} + scenarios={scenarios} + onSelectScenario={handleSelectScenario} /> )} diff --git a/packages/app/src/components/SimulationPanel.tsx b/packages/app/src/components/SimulationPanel.tsx index 53376b9..8b6ce36 100644 --- a/packages/app/src/components/SimulationPanel.tsx +++ b/packages/app/src/components/SimulationPanel.tsx @@ -1,8 +1,11 @@ import { useState, useCallback, useEffect } from 'react' import type { UseSimulationReturn } from '../hooks/useSimulation' +import type { TemplateScenario } from '../data/template-scenarios' export interface SimulationPanelProps { simulation: UseSimulationReturn + scenarios?: TemplateScenario[] + onSelectScenario?: (scenario: TemplateScenario | null) => void } const panelStyle: React.CSSProperties = { @@ -103,23 +106,91 @@ function StatusBadge({ status }: { status: string }) { } function buildCumulativeContext( - steps: { stepOutput?: { nodeId: string; value: unknown } }[], + steps: { + stepOutput?: { nodeId: string; value: unknown } + branchOutputs?: Record + }[], upToIndex: number, ): Record { const ctx: Record = {} for (let i = 0; i <= upToIndex; i++) { - const out = steps[i]?.stepOutput - if (out) ctx[out.nodeId] = out.value + const step = steps[i] + if (step?.stepOutput) ctx[step.stepOutput.nodeId] = step.stepOutput.value + if (step?.branchOutputs) { + for (const [branchId, value] of Object.entries(step.branchOutputs)) { + ctx[branchId] = value + } + } } return ctx } -export function SimulationPanel({ simulation }: SimulationPanelProps) { +const selectStyle: React.CSSProperties = { + width: '100%', + padding: '6px 8px', + fontSize: 12, + fontFamily: 'var(--fp-font-sans, system-ui, sans-serif)', + background: '#1C1B25', + color: '#E8E7F4', + border: '1px solid #2E2D3D', + borderRadius: 6, + cursor: 'pointer', +} + +export function SimulationPanel({ simulation, scenarios, onSelectScenario }: SimulationPanelProps) { const [inputText, setInputText] = useState('{}') const [fixturesText, setFixturesText] = useState('') const [showFixtures, setShowFixtures] = useState(false) const [showContext, setShowContext] = useState(false) const [inputError, setInputError] = useState(null) + const [selectedScenarioId, setSelectedScenarioId] = useState(null) + + const hasScenarios = scenarios && scenarios.length > 0 + const selectedScenario = hasScenarios + ? scenarios.find((s) => s.id === selectedScenarioId) ?? null + : null + + const handleSelectScenario = useCallback( + (scenarioId: string) => { + if (scenarioId === '') { + setSelectedScenarioId(null) + onSelectScenario?.(null) + return + } + const scenario = scenarios?.find((s) => s.id === scenarioId) + if (!scenario) return + setSelectedScenarioId(scenarioId) + setInputText(JSON.stringify(scenario.input, null, 2)) + setFixturesText(JSON.stringify(scenario.fixtures ?? {}, null, 2)) + if (scenario.fixtures && Object.keys(scenario.fixtures).length > 0) { + setShowFixtures(true) + } + setInputError(null) + onSelectScenario?.(scenario) + }, + [scenarios, onSelectScenario], + ) + + const handleInputChange = useCallback( + (value: string) => { + setInputText(value) + // Clear the scenario label but keep rules — user is customizing the input + if (selectedScenarioId) { + setSelectedScenarioId(null) + } + }, + [selectedScenarioId], + ) + + const handleFixturesChange = useCallback( + (value: string) => { + setFixturesText(value) + if (selectedScenarioId) { + setSelectedScenarioId(null) + } + }, + [selectedScenarioId], + ) const { isActive, @@ -136,6 +207,8 @@ export function SimulationPanel({ simulation }: SimulationPanelProps) { reset, setAutoPlay, isAutoPlaying, + playbackSpeed, + setPlaybackSpeed, } = simulation const handleRun = useCallback(() => { @@ -207,13 +280,38 @@ export function SimulationPanel({ simulation }: SimulationPanelProps) { + {hasScenarios && ( +
+ + + {selectedScenario && ( +
+ {selectedScenario.description} +
+ )} +
+ )} +