All files / web/src/components/flowchart MermaidViewer.tsx

0% Statements 0/108
0% Branches 0/1
0% Functions 0/1
0% Lines 0/108

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109                                                                                                                                                                                                                         
'use client'

import { useEffect, useRef, useState } from 'react'
import { css } from '../../../styled-system/css'

interface MermaidViewerProps {
  /** Raw mermaid content */
  mermaidContent: string
}

/**
 * Simple mermaid diagram viewer without node highlighting.
 * Used for static display of flowcharts.
 */
export function MermaidViewer({ mermaidContent }: MermaidViewerProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    if (!containerRef.current || !mermaidContent) return

    const render = async () => {
      const mermaid = (await import('mermaid')).default

      // Initialize mermaid with basic settings
      mermaid.initialize({
        startOnLoad: false,
        theme: 'base',
        themeVariables: {
          primaryColor: '#e3f2fd',
          primaryTextColor: '#1a1a1a',
          primaryBorderColor: '#90caf9',
          lineColor: '#444444',
          secondaryColor: '#fff8e1',
          tertiaryColor: '#e8f5e9',
        },
        flowchart: {
          curve: 'basis',
          nodeSpacing: 30,
          rankSpacing: 50,
          padding: 20,
        },
        securityLevel: 'loose',
      })

      // Generate unique ID for this render
      const id = `mermaid-${Date.now()}`

      try {
        // Sanitize content: convert escaped quotes that might have leaked through JSON parsing
        const sanitizedContent = mermaidContent
          .replace(/\\"/g, "'") // Convert \" to '
          .replace(/\\'/g, "'") // Convert \' to '

        const { svg } = await mermaid.render(id, sanitizedContent)
        if (containerRef.current) {
          containerRef.current.innerHTML = svg
          setError(null)
        }
      } catch (err) {
        console.error('Mermaid render error:', err)
        setError(err instanceof Error ? err.message : 'Failed to render diagram')
      } finally {
        // Clean up any orphaned mermaid elements in document.body
        // Mermaid creates hidden divs for rendering that can persist on error
        const orphanedElement = document.getElementById(id)
        if (orphanedElement && orphanedElement.parentElement === document.body) {
          orphanedElement.remove()
        }
      }
    }

    render()
  }, [mermaidContent])

  if (error) {
    return (
      <div
        className={css({
          padding: '4',
          color: { base: 'red.600', _dark: 'red.400' },
          fontSize: 'sm',
          textAlign: 'center',
        })}
      >
        <p>Failed to render flowchart:</p>
        <p className={css({ fontSize: 'xs', marginTop: '1' })}>{error}</p>
      </div>
    )
  }

  return (
    <div
      ref={containerRef}
      data-component="mermaid-viewer"
      className={css({
        width: '100%',
        minHeight: '200px',
        display: 'flex',
        justifyContent: 'center',
        '& svg': {
          maxWidth: '100%',
          height: 'auto',
        },
      })}
    />
  )
}