-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add interactive graph walker visualization #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
52f5298
be14867
c271bb7
5123bec
43dbfd0
05e03f5
7231187
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| '@ruminaider/flowprint-engine': minor | ||
| '@ruminaider/flowprint-editor': minor | ||
| --- | ||
|
|
||
| Add browser-safe simulation engine with shared walkGraph skeleton, AST interpreter, and pure rules evaluation core. Add SimulationContext to editor for node highlighting during simulation. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { test, expect } from '@playwright/test' | ||
|
|
||
| test.describe('simulation', () => { | ||
| test.beforeEach(async ({ page }) => { | ||
| await page.goto('/') | ||
|
|
||
| // Create a new blueprint so doc is loaded and Simulate button appears | ||
| await page.getByRole('button', { name: 'New Blueprint' }).click() | ||
| await page.getByLabel(/blueprint name/i).fill('sim-test') | ||
| await page.getByRole('button', { name: 'Create' }).click() | ||
|
|
||
| // Wait for editor to appear | ||
| await expect(page.getByText('sim-test', { exact: true })).toBeVisible() | ||
| }) | ||
|
|
||
| test('simulate button appears and opens panel', async ({ page }) => { | ||
| const simBtn = page.getByRole('button', { name: 'Simulate' }) | ||
| await expect(simBtn).toBeVisible() | ||
|
|
||
| await simBtn.click() | ||
|
|
||
| // Simulation panel should appear with input textarea | ||
| await expect(page.getByText('Input JSON')).toBeVisible() | ||
| await expect(page.getByRole('button', { name: 'Run' })).toBeVisible() | ||
| }) | ||
|
|
||
| test('run simulation shows step counter and highlights', async ({ page }) => { | ||
| // Open simulation panel | ||
| await page.getByRole('button', { name: 'Simulate' }).click() | ||
|
|
||
| // Run with default empty input | ||
| await page.getByRole('button', { name: 'Run' }).click() | ||
|
|
||
| // Step counter should appear | ||
| await expect(page.getByText(/Step \d+ of \d+/)).toBeVisible() | ||
|
|
||
| // At least one node should have an active highlight | ||
| await expect(page.locator('.fp-node--sim-active')).toBeVisible() | ||
| }) | ||
|
|
||
| test('stop simulation closes panel', async ({ page }) => { | ||
| await page.getByRole('button', { name: 'Simulate' }).click() | ||
| await page.getByRole('button', { name: 'Run' }).click() | ||
|
|
||
| // Wait for simulation to load | ||
| await expect(page.getByText(/Step \d+ of \d+/)).toBeVisible() | ||
|
|
||
| // Stop the simulation — panel closes entirely | ||
| await page.getByRole('button', { name: 'Stop' }).click() | ||
|
|
||
| // Panel should be gone | ||
| await expect(page.getByText(/Step \d+ of \d+/)).not.toBeVisible() | ||
| await expect(page.getByRole('button', { name: 'Simulate' })).toBeVisible() | ||
| }) | ||
|
|
||
| test('close simulation hides panel', async ({ page }) => { | ||
| await page.getByRole('button', { name: 'Simulate' }).click() | ||
| await expect(page.getByText('Input JSON')).toBeVisible() | ||
|
|
||
| // Close the panel | ||
| await page.getByRole('button', { name: 'Close' }).click() | ||
|
|
||
| // Panel should be gone, button should say Simulate (not Simulating) | ||
| await expect(page.getByText('Input JSON')).not.toBeVisible() | ||
| await expect(page.getByRole('button', { name: 'Simulate' })).toBeVisible() | ||
| }) | ||
|
|
||
| test('invalid JSON input shows error', async ({ page }) => { | ||
| await page.getByRole('button', { name: 'Simulate' }).click() | ||
|
|
||
| // Enter invalid JSON | ||
| const textarea = page.locator('textarea').first() | ||
| await textarea.fill('not valid json') | ||
|
|
||
| await page.getByRole('button', { name: 'Run' }).click() | ||
|
|
||
| // Error message should appear | ||
| await expect(page.getByText('Invalid JSON input')).toBeVisible() | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,23 +2,28 @@ import { useState, useCallback } from 'react' | |
| import { FlowprintEditor, useTheme, useSymbolSearch } from '@ruminaider/flowprint-editor' | ||
| import type { RulesDataMap } from '@ruminaider/flowprint-editor' | ||
| import '@ruminaider/flowprint-editor/styles.css' | ||
| import './styles/app.css' | ||
| import type { FlowprintDocument } from '@ruminaider/flowprint-schema' | ||
| import { Header } from './components/Header' | ||
| import { WelcomeScreen } from './components/WelcomeScreen' | ||
| import { NewBlueprintWizard } from './components/NewBlueprintWizard' | ||
| import { SettingsDialog } from './components/SettingsDialog' | ||
| import { SimulationPanel } from './components/SimulationPanel' | ||
| import { SimulationErrorBoundary } from './components/SimulationErrorBoundary' | ||
| import { UnsavedChangesGuard } from './components/UnsavedChangesGuard' | ||
| import { useFileManager } from './hooks/useFileManager' | ||
| import { useProjectDirectory } from './hooks/useProjectDirectory' | ||
| import { useRecentFiles } from './hooks/useRecentFiles' | ||
| import { useSettings } from './hooks/useSettings' | ||
| import { useSimulation } from './hooks/useSimulation' | ||
| import type { AppSettings } from './hooks/useSettings' | ||
| import type { RecentFile } from './hooks/useRecentFiles' | ||
|
|
||
| export function App() { | ||
| const [doc, setDoc] = useState<FlowprintDocument | null>(null) | ||
| const [wizardOpen, setWizardOpen] = useState(false) | ||
| const [settingsOpen, setSettingsOpen] = useState(false) | ||
| const [showSimPanel, setShowSimPanel] = useState(false) | ||
| const [error, setError] = useState<string | null>(null) | ||
| const [rulesDataMap, setRulesDataMap] = useState<RulesDataMap>({}) | ||
|
|
||
|
|
@@ -31,6 +36,20 @@ export function App() { | |
| codeSearchUrl: settings.codeSearchUrl || undefined, | ||
| }) | ||
|
|
||
| const simulation = useSimulation(doc, rulesDataMap) | ||
|
|
||
| // Gate on doc !== null only, not on rules presence | ||
| const canSimulate = doc !== null | ||
|
|
||
| const handleSimulate = useCallback(() => { | ||
| if (simulation.isActive) { | ||
| simulation.stop() | ||
| setShowSimPanel(false) | ||
| } else { | ||
| setShowSimPanel(true) | ||
| } | ||
| }, [simulation]) | ||
|
|
||
| const handleDocLoaded = useCallback( | ||
| (loaded: FlowprintDocument, fileName: string) => { | ||
| setDoc(loaded) | ||
|
|
@@ -112,11 +131,13 @@ export function App() { | |
| return | ||
| } | ||
| } | ||
| simulation.stop() | ||
| setShowSimPanel(false) | ||
| setDoc(null) | ||
| fileManager.setDirty(false) | ||
| setRulesDataMap({}) | ||
| setError(null) | ||
| }, [fileManager]) | ||
| }, [fileManager, simulation]) | ||
|
|
||
| return ( | ||
| <div | ||
|
|
@@ -198,6 +219,9 @@ export function App() { | |
| onSettings={() => { | ||
| setSettingsOpen(true) | ||
| }} | ||
| onSimulate={handleSimulate} | ||
| isSimulating={simulation.isActive || showSimPanel} | ||
| canSimulate={canSimulate} | ||
| onClose={handleClose} | ||
| /> | ||
| <div style={{ flex: 1, minHeight: 0 }}> | ||
|
|
@@ -207,11 +231,30 @@ export function App() { | |
| theme={settings.theme} | ||
| symbolSearch={symbolSearch ?? undefined} | ||
| rulesDataMap={rulesDataMap} | ||
| nodeHighlights={simulation.nodeHighlights} | ||
| showYamlPreview | ||
| showExportButton | ||
| style={{ width: '100%', height: '100%' }} | ||
| /> | ||
| </div> | ||
| {showSimPanel && ( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟠 Finding 6 — SimulationPanel has no React error boundary (error-hunter)
Fix: Wrap in its own error boundary: <SimulationErrorBoundary onReset={() => { simulation.stop(); setShowSimPanel(false) }}>
<SimulationPanel simulation={...} />
</SimulationErrorBoundary>
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 43dbfd0. Added |
||
| <SimulationErrorBoundary | ||
| onReset={() => { | ||
| simulation.stop() | ||
| setShowSimPanel(false) | ||
| }} | ||
| > | ||
| <SimulationPanel | ||
| simulation={{ | ||
| ...simulation, | ||
| stop: () => { | ||
| simulation.stop() | ||
| setShowSimPanel(false) | ||
| }, | ||
| }} | ||
| /> | ||
| </SimulationErrorBoundary> | ||
| )} | ||
| </> | ||
| )} | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Finding 2 — CLAUDE.md dependency graph violation (code-reviewer)
CLAUDE.md states
appdepends oneditor + schema. This line adds@ruminaider/flowprint-engineas a direct dependency, changing the documented dependency graph without updating CLAUDE.md.Fix: Update CLAUDE.md to document
app → editor + schema + engine, or restructure to avoid the direct dependency.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 5123bec. Updated CLAUDE.md to document
app → editor + schema + engine.