diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..db3318cb Binary files /dev/null and b/.DS_Store differ diff --git a/package.json b/package.json index ea65e265..2a9fab57 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "A Model Context Protocol server that acts as an intelligent conversation state manager and development guide for LLMs", "main": "dist/index.js", "bin": { - "responsible-vibe-mcp": "dist/index.js" + "responsible-vibe-mcp": "dist/index.js", + "workflow-visualizer": "workflow-visualizer/bin/visualizer.js" }, "type": "module", "engines": { @@ -13,6 +14,7 @@ "files": [ "dist/**/*", "resources/**/*", + "workflow-visualizer/**/*", "README.md", "SYSTEM_PROMPT.md", "LOGGING.md", @@ -24,7 +26,8 @@ "url": "git+https://github.com/mrsimpson/vibe-feature-mcp.git" }, "scripts": { - "build": "tsc", + "build": "tsc && npm run build:visualizer", + "build:visualizer": "cd workflow-visualizer && npm install && npm run build", "inspector": "npx @modelcontextprotocol/inspector", "dev": "tsc --watch", "clean": "rm -rf dist", diff --git a/workflow-visualizer/bin/visualizer.js b/workflow-visualizer/bin/visualizer.js new file mode 100755 index 00000000..6aa1a8f2 --- /dev/null +++ b/workflow-visualizer/bin/visualizer.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node + +import { spawn } from 'child_process'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const visualizerDir = join(__dirname, '..'); + +console.log('šŸš€ Starting Workflow Visualizer...'); +console.log('šŸ“ Directory:', visualizerDir); + +// Check if dependencies are installed +import { existsSync } from 'fs'; +if (!existsSync(join(visualizerDir, 'node_modules'))) { + console.log('šŸ“¦ Installing dependencies...'); + const install = spawn('npm', ['install'], { + cwd: visualizerDir, + stdio: 'inherit' + }); + + install.on('close', (code) => { + if (code === 0) { + startServer(); + } else { + console.error('āŒ Failed to install dependencies'); + process.exit(1); + } + }); +} else { + startServer(); +} + +function startServer() { + console.log('🌐 Starting development server...'); + const server = spawn('npm', ['run', 'dev'], { + cwd: visualizerDir, + stdio: 'inherit' + }); + + server.on('close', (code) => { + console.log(`Server exited with code ${code}`); + }); + + // Handle Ctrl+C + process.on('SIGINT', () => { + console.log('\nšŸ‘‹ Shutting down visualizer...'); + server.kill('SIGINT'); + process.exit(0); + }); +} diff --git a/workflow-visualizer/package-lock.json b/workflow-visualizer/package-lock.json index eeab8bc0..56f7f8f3 100644 --- a/workflow-visualizer/package-lock.json +++ b/workflow-visualizer/package-lock.json @@ -12,6 +12,9 @@ "js-yaml": "^4.1.0", "plantuml-encoder": "1.4.0" }, + "bin": { + "workflow-visualizer": "bin/visualizer.js" + }, "devDependencies": { "@types/d3": "^7.4.3", "@types/js-yaml": "^4.0.9", diff --git a/workflow-visualizer/package.json b/workflow-visualizer/package.json index 4f6ab1d6..dfde6412 100644 --- a/workflow-visualizer/package.json +++ b/workflow-visualizer/package.json @@ -3,6 +3,9 @@ "version": "1.0.0", "description": "Web app for visualizing responsible-vibe workflow state machines", "type": "module", + "bin": { + "workflow-visualizer": "bin/visualizer.js" + }, "scripts": { "dev": "vite", "build": "tsc && vite build", diff --git a/workflow-visualizer/src/main.ts b/workflow-visualizer/src/main.ts index 6c907a60..156505f7 100644 --- a/workflow-visualizer/src/main.ts +++ b/workflow-visualizer/src/main.ts @@ -8,6 +8,7 @@ import { FileUploadHandler } from './services/FileUploadHandler'; import { ErrorHandler } from './utils/ErrorHandler'; import { PlantUMLRenderer } from './visualization/PlantUMLRenderer'; import { getRequiredElement } from './utils/DomHelpers'; +import type { InteractionEvent } from './types/ui-types'; import { YamlStateMachine, AppState, TransitionData } from './types/ui-types'; class WorkflowVisualizerApp { @@ -53,7 +54,7 @@ class WorkflowVisualizerApp { }); } else if (elementType === 'transition') { this.handleElementClick({ - elementType: 'link', + elementType: 'transition', elementId: elementId, data: data }); @@ -203,23 +204,12 @@ class WorkflowVisualizerApp { if (event.elementType === 'node' && event.data) { console.log('Selecting state:', event.elementId); this.selectState(event.elementId!, event.data); - } else if (event.elementType === 'link' && event.data) { + } else if (event.elementType === 'transition' && event.data) { console.log('Selecting transition:', event.elementId); this.selectTransition(event.elementId!, event.data); } } - /** - * Handle element hover in the diagram - */ - private handleElementHover(event: InteractionEvent): void { - // For now, just log hover events - // Could be extended to show tooltips - if (event.type === 'hover') { - console.log('Element hovered:', event.elementType, event.elementId); - } - } - /** * Select a state node */ @@ -272,16 +262,6 @@ class WorkflowVisualizerApp { } } - /** - * Highlight a transition path - */ - private highlightTransitionPath(fromState: string, toState: string): void { - // Note: PlantUML diagrams don't support interactive path highlighting - // Path information is shown in the side panel instead - const pathElements = [fromState, toState]; - this.appState.highlightedPath = pathElements; - } - /** * Update the side panel content */ @@ -519,116 +499,6 @@ class WorkflowVisualizerApp { } /** - * Render state details - */ - private renderStateDetails(stateId: string, stateData: any, container: HTMLElement): void { - const workflow = this.appState.currentWorkflow!; - const isInitial = stateId === workflow.initial_state; - - container.innerHTML = ` -
-

- ${stateId} - ${isInitial ? 'Initial' : ''} -

-

${stateData.description}

-
- -
-

Default Instructions

-
${stateData.default_instructions}
-
- -
-

Transitions (${stateData.transitions.length})

- -
- `; - - // Add click handlers to transitions - const transitionItems = container.querySelectorAll('.clickable-transition'); - transitionItems.forEach(item => { - item.addEventListener('click', (e) => { - e.stopPropagation(); - const fromState = item.getAttribute('data-from'); - const toState = item.getAttribute('data-to'); - const trigger = item.getAttribute('data-trigger'); - - if (fromState && toState && trigger) { - console.log('Side panel transition clicked:', `${fromState}->${toState}`); - - // Find the full transition data - const fullTransition = stateData.transitions.find((t: any) => - t.to === toState && t.trigger === trigger - ); - - if (fullTransition) { - this.selectTransition(`${fromState}->${toState}`, { - from: fromState, - to: toState, - trigger: trigger, - instructions: fullTransition.instructions, - additional_instructions: fullTransition.additional_instructions, - transition_reason: fullTransition.transition_reason - }); - } - } - }); - - // Add hover effects - item.addEventListener('mouseenter', () => { - (item as HTMLElement).style.backgroundColor = '#f0f9ff'; - (item as HTMLElement).style.cursor = 'pointer'; - }); - - item.addEventListener('mouseleave', () => { - (item as HTMLElement).style.backgroundColor = ''; - (item as HTMLElement).style.cursor = ''; - }); - }); - } - - /** - * Render transition details - */ - private renderTransitionDetails(transitionData: TransitionData, container: HTMLElement): void { - container.innerHTML = ` -
-

Transition: ${transitionData.trigger}

-

- ${transitionData.from} → ${transitionData.to} -

-
- -
-

Reason

-

${transitionData.transition_reason}

-
- - ${transitionData.instructions ? ` -
-

Instructions

-
${transitionData.instructions}
-
- ` : ''} - - ${transitionData.additional_instructions ? ` -
-

Additional Instructions

-
${transitionData.additional_instructions}
-
- ` : ''} - `; - } - /** * Clear the visualization */ diff --git a/workflow-visualizer/src/types/plantuml-encoder.d.ts b/workflow-visualizer/src/types/plantuml-encoder.d.ts new file mode 100644 index 00000000..3e317147 --- /dev/null +++ b/workflow-visualizer/src/types/plantuml-encoder.d.ts @@ -0,0 +1,4 @@ +declare module 'plantuml-encoder' { + export function encode(plantuml: string): string; + export function decode(encoded: string): string; +} diff --git a/workflow-visualizer/src/types/ui-types.ts b/workflow-visualizer/src/types/ui-types.ts index b895d9ae..785bf76f 100644 --- a/workflow-visualizer/src/types/ui-types.ts +++ b/workflow-visualizer/src/types/ui-types.ts @@ -8,6 +8,16 @@ import type { YamlStateMachine, YamlState, YamlTransition } from '../../../src/s // Re-export for convenience export type { YamlStateMachine, YamlState, YamlTransition }; +/** + * Interaction event for diagram elements + */ +export interface InteractionEvent { + elementType: 'node' | 'edge' | 'transition'; + elementId?: string; + data?: any; + originalEvent?: Event; +} + /** * Application state interface */ diff --git a/workflow-visualizer/src/visualization/PlantUMLRenderer.ts b/workflow-visualizer/src/visualization/PlantUMLRenderer.ts index 03a4306d..463fce34 100644 --- a/workflow-visualizer/src/visualization/PlantUMLRenderer.ts +++ b/workflow-visualizer/src/visualization/PlantUMLRenderer.ts @@ -1,10 +1,9 @@ -import { YamlStateMachine } from '../types/workflow-types'; +import { YamlStateMachine, YamlState } from '../types/ui-types'; import * as plantumlEncoder from 'plantuml-encoder'; export class PlantUMLRenderer { private container: HTMLElement; private onElementClick?: (elementType: 'state' | 'transition', elementId: string, data?: any) => void; - private currentWorkflow?: YamlStateMachine; constructor(container: HTMLElement) { this.container = container; @@ -23,8 +22,6 @@ export class PlantUMLRenderer { public async renderWorkflow(workflow: YamlStateMachine): Promise { console.log(`Rendering workflow with PlantUML: ${workflow.name}`); - this.currentWorkflow = workflow; - // Clear container and set up scrollable area this.container.innerHTML = ''; this.container.style.overflow = 'auto'; @@ -91,7 +88,7 @@ export class PlantUMLRenderer { lines.push(''); // Add states with descriptions - Object.entries(workflow.states).forEach(([stateName, stateConfig]) => { + Object.entries(workflow.states).forEach(([stateName, stateConfig]: [string, YamlState]) => { if (stateConfig.description) { lines.push(`${stateName} : ${stateConfig.description}`); } @@ -99,7 +96,7 @@ export class PlantUMLRenderer { lines.push(''); // Add transitions - Object.entries(workflow.states).forEach(([stateName, stateConfig]) => { + Object.entries(workflow.states).forEach(([stateName, stateConfig]: [string, YamlState]) => { if (stateConfig.transitions) { stateConfig.transitions.forEach(transition => { const label = transition.trigger.replace(/_/g, ' '); @@ -177,12 +174,12 @@ export class PlantUMLRenderer { container.appendChild(svgContainer); // Add simplified interactive cards (no transitions) - this.addSimplifiedInteractiveCards(container.parentElement!, workflow); + this.addSimplifiedInteractiveCards(container.parentElement!); } catch (error) { console.error('Failed to load interactive SVG:', error); this.showError('Failed to load interactive diagram. Using fallback.'); - this.renderFallbackDiagram(workflow); + this.renderFallbackDiagram(); } } @@ -201,8 +198,8 @@ export class PlantUMLRenderer { const stateName = groupId; // Make the entire group clickable - group.style.cursor = 'pointer'; - group.style.transition = 'all 0.2s ease'; + (group as HTMLElement).style.cursor = 'pointer'; + (group as HTMLElement).style.transition = 'all 0.2s ease'; // Find the rect/shape element for hover effects const shape = group.querySelector('rect, ellipse, polygon'); @@ -253,8 +250,8 @@ export class PlantUMLRenderer { // Verify these are valid states if (states.includes(fromState) && states.includes(toState)) { // Make the entire link group clickable - linkGroup.style.cursor = 'pointer'; - linkGroup.style.transition = 'all 0.2s ease'; + (linkGroup as HTMLElement).style.cursor = 'pointer'; + (linkGroup as HTMLElement).style.transition = 'all 0.2s ease'; // Find path and text elements for hover effects const pathEl = linkGroup.querySelector('path'); @@ -317,7 +314,7 @@ export class PlantUMLRenderer { /** * Add simplified interactive cards (states only, no transitions) */ - private addSimplifiedInteractiveCards(container: HTMLElement, workflow: YamlStateMachine): void { + private addSimplifiedInteractiveCards(container: HTMLElement): void { const instructionDiv = document.createElement('div'); instructionDiv.style.marginTop = '15px'; instructionDiv.style.textAlign = 'center'; @@ -335,7 +332,7 @@ export class PlantUMLRenderer { /** * Render fallback diagram if PlantUML fails */ - private renderFallbackDiagram(workflow: YamlStateMachine): void { + private renderFallbackDiagram(): void { const fallbackDiv = document.createElement('div'); fallbackDiv.style.padding = '20px'; fallbackDiv.style.border = '2px dashed #94a3b8'; @@ -351,7 +348,6 @@ export class PlantUMLRenderer { `; this.container.appendChild(fallbackDiv); - this.addInteractiveOverlay(fallbackDiv, workflow); } /** diff --git a/workflow-visualizer/src/visualization/StateRenderer.ts b/workflow-visualizer/src/visualization/StateRenderer.ts index e34f46be..339e1bfa 100644 --- a/workflow-visualizer/src/visualization/StateRenderer.ts +++ b/workflow-visualizer/src/visualization/StateRenderer.ts @@ -38,8 +38,8 @@ export class StateRenderer { // Add circles for states nodeEnter.append('circle') - .attr('cx', d => d.x) - .attr('cy', d => d.y) + .attr('cx', d => d.x || 0) + .attr('cy', d => d.y || 0) .attr('r', d => this.getNodeRadius(d)) .style('fill', d => this.getNodeFill(d)) .style('stroke', d => this.getNodeStroke(d)) @@ -48,8 +48,8 @@ export class StateRenderer { // Add labels for states nodeEnter.append('text') .attr('class', 'state-label') - .attr('x', d => d.x) - .attr('y', d => d.y) + .attr('x', d => d.x || 0) + .attr('y', d => d.y || 0) .attr('text-anchor', 'middle') .attr('dominant-baseline', 'central') .style('font-size', this.style.text.fontSize)