feat: add mermaid diagram support
This commit is contained in:
		
							
								
								
									
										73
									
								
								App.tsx
									
									
									
									
									
								
							
							
						
						
									
										73
									
								
								App.tsx
									
									
									
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
 | 
			
		||||
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
 | 
			
		||||
import { Dialog } from "@headlessui/react";
 | 
			
		||||
import { Cog6ToothIcon } from "@heroicons/react/24/outline";
 | 
			
		||||
import { Exercise, CheckResult, ExercisePartType } from './types';
 | 
			
		||||
@ -18,6 +18,7 @@ import Editor from 'react-simple-code-editor';
 | 
			
		||||
import Prism from 'prismjs/components/prism-core';
 | 
			
		||||
import 'prismjs/components/prism-clike';
 | 
			
		||||
import 'prismjs/components/prism-c';
 | 
			
		||||
import mermaid from 'mermaid';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 // --- GEMINI API SETUP & HELPERS ---
 | 
			
		||||
@ -167,6 +168,68 @@ const TextAreaEditor: React.FC<{
 | 
			
		||||
    )
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MermaidDiagram: React.FC<{ value: string; onChange: (value: string) => void; placeholder?: string }> = ({ value, onChange, placeholder }) => {
 | 
			
		||||
    const [diagramHtml, setDiagramHtml] = useState<string>('');
 | 
			
		||||
    const [error, setError] = useState<string>('');
 | 
			
		||||
    const renderCountRef = useRef(0);
 | 
			
		||||
    
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        mermaid.initialize({ 
 | 
			
		||||
            startOnLoad: false,
 | 
			
		||||
            theme: 'default',
 | 
			
		||||
            securityLevel: 'loose'
 | 
			
		||||
        });
 | 
			
		||||
    }, []);
 | 
			
		||||
    
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (value.trim()) {
 | 
			
		||||
            const renderDiagram = async () => {
 | 
			
		||||
                try {
 | 
			
		||||
                    setError('');
 | 
			
		||||
                    renderCountRef.current += 1;
 | 
			
		||||
                    const uniqueId = `mermaid-diagram-${renderCountRef.current}`;
 | 
			
		||||
                    const { svg } = await mermaid.render(uniqueId, value);
 | 
			
		||||
                    setDiagramHtml(svg);
 | 
			
		||||
                } catch (err) {
 | 
			
		||||
                    setError(err instanceof Error ? err.message : 'Mermaid diagram rendering failed');
 | 
			
		||||
                    setDiagramHtml('');
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
            renderDiagram();
 | 
			
		||||
        } else {
 | 
			
		||||
            setDiagramHtml('');
 | 
			
		||||
            setError('');
 | 
			
		||||
        }
 | 
			
		||||
    }, [value]);
 | 
			
		||||
    
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="space-y-3">
 | 
			
		||||
            <textarea
 | 
			
		||||
                value={value}
 | 
			
		||||
                onChange={(e) => onChange(e.target.value)}
 | 
			
		||||
                placeholder={placeholder}
 | 
			
		||||
                className="w-full h-48 p-3 bg-slate-50 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 font-mono text-slate-800"
 | 
			
		||||
                aria-label="Mermaid diagram input"
 | 
			
		||||
            />
 | 
			
		||||
            {error && (
 | 
			
		||||
                <div className="p-3 bg-red-50 border-2 border-red-200 rounded-lg">
 | 
			
		||||
                    <p className="text-red-700 text-sm font-medium">Mermaid Error:</p>
 | 
			
		||||
                    <p className="text-red-600 text-sm">{error}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            )}
 | 
			
		||||
            {diagramHtml && (
 | 
			
		||||
                <div className="p-4 bg-white border-2 border-slate-200 rounded-lg">
 | 
			
		||||
                    <p className="text-slate-600 text-sm mb-2 font-medium">Preview:</p>
 | 
			
		||||
                    <div 
 | 
			
		||||
                        className="mermaid-preview" 
 | 
			
		||||
                        dangerouslySetInnerHTML={{ __html: diagramHtml }}
 | 
			
		||||
                    />
 | 
			
		||||
                </div>
 | 
			
		||||
            )}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
interface CodeDiffViewerProps {
 | 
			
		||||
    oldCode: string;
 | 
			
		||||
@ -320,11 +383,17 @@ const ExerciseSheet: React.FC<ExerciseSheetProps> = ({ exercise, onNext, ai }) =
 | 
			
		||||
                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
			
		||||
                                        placeholder={`// Code für Teil ${part.id}) hier eingeben...`}
 | 
			
		||||
                                    />
 | 
			
		||||
                                ) : part.type === 'mermaid' ? (
 | 
			
		||||
                                    <MermaidDiagram
 | 
			
		||||
                                        value={inputs[part.id] || ''}
 | 
			
		||||
                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
			
		||||
                                        placeholder="Mermaid-Diagramm hier eingeben..."
 | 
			
		||||
                                    />
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <TextAreaEditor
 | 
			
		||||
                                        value={inputs[part.id] || ''}
 | 
			
		||||
                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
			
		||||
                                        placeholder={part.type === 'mermaid' ? 'Antwort in Mermaid-Syntax hier eingeben...' : `Antwort hier eingeben...`}
 | 
			
		||||
                                        placeholder={`Antwort hier eingeben...`}
 | 
			
		||||
                                    />
 | 
			
		||||
                                )}
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
@ -375,4 +375,146 @@ CMD [ "serve", "-s", "dist", "-l", "8000" ]
 | 
			
		||||
+GEMINI_API_KEY=
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 5. Optional: Adding Mermaid Diagram Support
 | 
			
		||||
 | 
			
		||||
**Objective**: To provide interactive Mermaid diagram rendering for exercises that require diagram creation, enhancing the user experience for architecture-related questions.
 | 
			
		||||
 | 
			
		||||
**Previous State**: The application had 'mermaid' as a defined exercise type in `types.ts`, but no actual Mermaid rendering component was implemented.
 | 
			
		||||
 | 
			
		||||
**Changes Made**:
 | 
			
		||||
 | 
			
		||||
- **Mermaid Dependency**: Added `mermaid` package (version 11.8.1) to handle diagram rendering.
 | 
			
		||||
- **MermaidDiagram Component**: Created a new React component that:
 | 
			
		||||
  - Provides a textarea for Mermaid syntax input
 | 
			
		||||
  - Renders diagrams in real-time as the user types
 | 
			
		||||
  - Shows error messages for invalid syntax
 | 
			
		||||
  - Displays a preview of the rendered diagram
 | 
			
		||||
  - Uses unique IDs for each render to prevent caching issues
 | 
			
		||||
- **Exercise Integration**: Modified the `ExerciseSheet` component to use `MermaidDiagram` for exercises with `type: 'mermaid'`
 | 
			
		||||
 | 
			
		||||
**Step-by-step Actions**:
 | 
			
		||||
 | 
			
		||||
1. Add `mermaid` import to `App.tsx`
 | 
			
		||||
2. Create the `MermaidDiagram` component with state management for diagram HTML and errors
 | 
			
		||||
3. Implement `useEffect` hooks to initialize Mermaid and handle diagram rendering
 | 
			
		||||
4. Add unique ID generation using `useRef` to prevent render caching issues
 | 
			
		||||
5. Update the exercise rendering logic to conditionally show `MermaidDiagram` for mermaid exercises
 | 
			
		||||
6. Add appropriate styling and error handling for the diagram preview
 | 
			
		||||
 | 
			
		||||
**Code Changes (App.tsx)**:
 | 
			
		||||
 | 
			
		||||
```diff
 | 
			
		||||
--- a/App.tsx
 | 
			
		||||
+++ b/App.tsx
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
-import React, { useState, useEffect, useCallback, useMemo } from 'react';
 | 
			
		||||
+import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
 | 
			
		||||
 import { Dialog } from "@headlessui/react";
 | 
			
		||||
 import { Cog6ToothIcon } from "@heroicons/react/24/outline";
 | 
			
		||||
 import { Exercise, CheckResult, ExercisePartType } from './types';
 | 
			
		||||
@@ -19,6 +19,7 @@ import Editor from 'react-simple-code-editor';
 | 
			
		||||
 import Prism from 'prismjs/components/prism-core';
 | 
			
		||||
 import 'prismjs/components/prism-clike';
 | 
			
		||||
 import 'prismjs/components/prism-c';
 | 
			
		||||
+import mermaid from 'mermaid';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
  // --- GEMINI API SETUP & HELPERS ---
 | 
			
		||||
@@ -168,6 +169,47 @@ const TextAreaEditor: React.FC<{
 | 
			
		||||
     )
 | 
			
		||||
 };
 | 
			
		||||
 | 
			
		||||
+const MermaidDiagram: React.FC<{ value: string; onChange: (value: string) => void; placeholder?: string }> = ({ value, onChange, placeholder }) => {
 | 
			
		||||
+    const [diagramHtml, setDiagramHtml] = useState<string>('');
 | 
			
		||||
+    const [error, setError] = useState<string>('');
 | 
			
		||||
+    const renderCountRef = useRef(0);
 | 
			
		||||
+    
 | 
			
		||||
+    useEffect(() => {
 | 
			
		||||
+        mermaid.initialize({ 
 | 
			
		||||
+            startOnLoad: false,
 | 
			
		||||
+            theme: 'default',
 | 
			
		||||
+            securityLevel: 'loose'
 | 
			
		||||
+        });
 | 
			
		||||
+    }, []);
 | 
			
		||||
+    
 | 
			
		||||
+    useEffect(() => {
 | 
			
		||||
+        if (value.trim()) {
 | 
			
		||||
+            const renderDiagram = async () => {
 | 
			
		||||
+                try {
 | 
			
		||||
+                    setError('');
 | 
			
		||||
+                    renderCountRef.current += 1;
 | 
			
		||||
+                    const uniqueId = `mermaid-diagram-${renderCountRef.current}`;
 | 
			
		||||
+                    const { svg } = await mermaid.render(uniqueId, value);
 | 
			
		||||
+                    setDiagramHtml(svg);
 | 
			
		||||
+                } catch (err) {
 | 
			
		||||
+                    setError(err instanceof Error ? err.message : 'Mermaid diagram rendering failed');
 | 
			
		||||
+                    setDiagramHtml('');
 | 
			
		||||
+                }
 | 
			
		||||
+            };
 | 
			
		||||
+            renderDiagram();
 | 
			
		||||
+        } else {
 | 
			
		||||
+            setDiagramHtml('');
 | 
			
		||||
+            setError('');
 | 
			
		||||
+        }
 | 
			
		||||
+    }, [value]);
 | 
			
		||||
+    
 | 
			
		||||
+    return (
 | 
			
		||||
+        <div className="space-y-3">
 | 
			
		||||
+            <textarea
 | 
			
		||||
+                value={value}
 | 
			
		||||
+                onChange={(e) => onChange(e.target.value)}
 | 
			
		||||
+                placeholder={placeholder}
 | 
			
		||||
+                className="w-full h-48 p-3 bg-slate-50 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 font-mono text-slate-800"
 | 
			
		||||
+                aria-label="Mermaid diagram input"
 | 
			
		||||
+            />
 | 
			
		||||
+            {error && (
 | 
			
		||||
+                <div className="p-3 bg-red-50 border-2 border-red-200 rounded-lg">
 | 
			
		||||
+                    <p className="text-red-700 text-sm font-medium">Mermaid Error:</p>
 | 
			
		||||
+                    <p className="text-red-600 text-sm">{error}</p>
 | 
			
		||||
+                </div>
 | 
			
		||||
+            )}
 | 
			
		||||
+            {diagramHtml && (
 | 
			
		||||
+                <div className="p-4 bg-white border-2 border-slate-200 rounded-lg">
 | 
			
		||||
+                    <p className="text-slate-600 text-sm mb-2 font-medium">Preview:</p>
 | 
			
		||||
+                    <div 
 | 
			
		||||
+                        className="mermaid-preview" 
 | 
			
		||||
+                        dangerouslySetInnerHTML={{ __html: diagramHtml }}
 | 
			
		||||
+                    />
 | 
			
		||||
+                </div>
 | 
			
		||||
+            )}
 | 
			
		||||
+        </div>
 | 
			
		||||
+    );
 | 
			
		||||
+};
 | 
			
		||||
 | 
			
		||||
@@ -376,12 +418,17 @@ const ExerciseSheet: React.FC<ExerciseSheetProps> = ({ exercise, onNext, ai })
 | 
			
		||||
                                         placeholder={`// Code für Teil ${part.id}) hier eingeben...`}
 | 
			
		||||
                                     />
 | 
			
		||||
-                                ) : (
 | 
			
		||||
+                                ) : part.type === 'mermaid' ? (
 | 
			
		||||
+                                    <MermaidDiagram
 | 
			
		||||
+                                        value={inputs[part.id] || ''}
 | 
			
		||||
+                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
			
		||||
+                                        placeholder="Mermaid-Diagramm hier eingeben..."
 | 
			
		||||
+                                    />
 | 
			
		||||
+                                ) : (
 | 
			
		||||
                                     <TextAreaEditor
 | 
			
		||||
                                         value={inputs[part.id] || ''}
 | 
			
		||||
                                         onChange={(value) => handleInputChange(part.id, value)}
 | 
			
		||||
-                                        placeholder={part.type === 'mermaid' ? 'Antwort in Mermaid-Syntax hier eingeben...' : `Antwort hier eingeben...`}
 | 
			
		||||
+                                        placeholder={`Antwort hier eingeben...`}
 | 
			
		||||
                                     />
 | 
			
		||||
                                 )}
 | 
			
		||||
                             </div>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
**Key Features**:
 | 
			
		||||
 | 
			
		||||
- **Real-time Preview**: Users can see their Mermaid diagram rendered as they type
 | 
			
		||||
- **Error Handling**: Clear error messages for invalid Mermaid syntax
 | 
			
		||||
- **Unique Rendering**: Each render uses a unique ID to prevent caching issues that could cause diagrams to disappear
 | 
			
		||||
- **Consistent Styling**: Matches the existing design language of the application
 | 
			
		||||
- **Accessibility**: Proper ARIA labels and semantic HTML structure
 | 
			
		||||
 | 
			
		||||
This enhancement makes the application more suitable for software architecture exercises that require diagram creation and visualization.
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
    "@headlessui/react": "^2.2.4",
 | 
			
		||||
    "@heroicons/react": "^2.2.0",
 | 
			
		||||
    "diff": "5.2.0",
 | 
			
		||||
    "mermaid": "^11.8.1",
 | 
			
		||||
    "prismjs": "1.29.0",
 | 
			
		||||
    "react": "^19.1.0",
 | 
			
		||||
    "react-dom": "^19.1.0",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1001
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1001
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user