// Copyright (c) 2024 Chris Pressey, Cat's Eye Technologies
// This file is distributed under an MIT license. See LICENSES directory:
// SPDX-License-Identifier: LicenseRef-MIT-X-StateLab
import React, { useState, useEffect, useRef } from 'react';
import mermaid from 'mermaid';
const StateDiagramExecutor = () => {
const [mermaidInput, setMermaidInput] = useState(`stateDiagram-v2
[*] --> Idle
Idle --> Selected: select_product
Selected --> Contacting_Bank: present_payment
Contacting_Bank --> Vending: payment_accepted
Contacting_Bank --> Declined: payment_declined
Declined --> Selected: 10sec
Vending --> Thank_You: vend_complete
Thank_You --> Idle: 10sec`);
const [currentState, setCurrentState] = useState('');
const [stateTransitions, setStateTransitions] = useState({});
const [svgContent, setSvgContent] = useState('');
const mermaidRef = useRef(null);
useEffect(() => {
mermaid.initialize({
startOnLoad: true,
securityLevel: 'loose',
theme: 'forest',
fontFamily: 'monospace',
});
updateDiagram();
}, [mermaidInput]);
const updateDiagram = async () => {
try {
const { svg } = await mermaid.render('state-diagram', mermaidInput);
setSvgContent(svg);
parseStateTransitions(mermaidInput);
} catch (error) {
console.error('Error rendering diagram:', error);
}
};
const parseStateTransitions = (input) => {
const lines = input.split('\n');
const transitions = {};
let initialState = '';
// First pass: collect all states that appear in transitions
const allStates = new Set();
lines.forEach(line => {
const transitionMatch = line.match(/^\s*(\S+)\s*-->\s*(\S+)\s*:\s*(\S+)/);
if (transitionMatch) {
const [, from, to] = transitionMatch;
if (from !== '[*]') allStates.add(from);
if (to !== '[*]') allStates.add(to);
} else {
const startMatch = line.match(/^\s*\[\*\]\s*-->\s*(\S+)/);
if (startMatch) {
initialState = startMatch[1];
allStates.add(startMatch[1]);
}
}
});
// Second pass: build transitions
lines.forEach(line => {
const transitionMatch = line.match(/^\s*(\S+)\s*-->\s*(\S+)\s*:\s*(\S+)/);
if (transitionMatch) {
const [, from, to, label] = transitionMatch;
if (from !== '[*]') {
if (!transitions[from]) transitions[from] = [];
transitions[from].push({ to, label });
}
}
});
setStateTransitions(transitions);
// Check if current state still exists in the new diagram
if (!currentState || !allStates.has(currentState)) {
setCurrentState(initialState);
}
};
const handleTransition = (nextState) => {
setCurrentState(nextState);
};
return (
<div style={{
fontFamily: 'Arial, sans-serif',
padding: '20px',
maxWidth: '1200px',
margin: '0 auto',
}}>
<h1 style={{ textAlign: 'center', marginBottom: '20px' }}>StateLab</h1>
<div style={{ display: 'flex', gap: '20px' }}>
<div style={{ flex: '1', display: 'flex', flexDirection: 'column', gap: '20px' }}>
<textarea
value={mermaidInput}
onChange={(e) => setMermaidInput(e.target.value)}
placeholder="Enter Mermaid.js state diagram code here..."
style={{
width: '100%',
height: '200px',
padding: '10px',
fontSize: '12px',
border: '1px solid #ccc',
borderRadius: '4px',
autocomplete: 'off',
autocorrect: 'off',
autocapitalize: 'off',
spellcheck: 'false'
}}
/>
<div className="current-state" style={{
padding: '15px',
backgroundColor: '#f0f0f0',
borderRadius: '4px',
}}>
<h2 style={{ marginTop: '0', marginBottom: '10px' }}>Current State</h2>
<p style={{ fontSize: '18px', fontWeight: 'bold' }}>{currentState || 'Not started'}</p>
</div>
<div className="transitions" style={{ display: 'flex', flexWrap: 'wrap', gap: '10px' }}>
{Object.entries(stateTransitions).map(([state, transitions]) => (
transitions.map(({ to, label }) => (
<button
key={`${state}-${to}-${label}`}
onClick={() => handleTransition(to)}
disabled={currentState !== state}
style={{
padding: '8px 12px',
fontSize: '14px',
backgroundColor: currentState === state ? '#4CAF50' : '#ddd',
color: currentState === state ? 'white' : 'black',
border: 'none',
borderRadius: '4px',
cursor: currentState === state ? 'pointer' : 'not-allowed',
}}
>
{label}
</button>
))
))}
</div>
</div>
<div
ref={mermaidRef}
dangerouslySetInnerHTML={{ __html: svgContent }}
className="mermaid-diagram"
style={{ flex: '1', minWidth: '400px' }}
/>
</div>
</div>
);
};
export default StateDiagramExecutor;