git @ Cat's Eye Technologies StateLab / master src / StateDiagramExecutor.jsx
master

Tree @master (Download .tar.gz)

StateDiagramExecutor.jsx @masterraw · history · blame

// 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;