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 { Dialog } from "@headlessui/react";
 | 
				
			||||||
import { Cog6ToothIcon } from "@heroicons/react/24/outline";
 | 
					import { Cog6ToothIcon } from "@heroicons/react/24/outline";
 | 
				
			||||||
import { Exercise, CheckResult, ExercisePartType } from './types';
 | 
					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 Prism from 'prismjs/components/prism-core';
 | 
				
			||||||
import 'prismjs/components/prism-clike';
 | 
					import 'prismjs/components/prism-clike';
 | 
				
			||||||
import 'prismjs/components/prism-c';
 | 
					import 'prismjs/components/prism-c';
 | 
				
			||||||
 | 
					import mermaid from 'mermaid';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 // --- GEMINI API SETUP & HELPERS ---
 | 
					 // --- 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 {
 | 
					interface CodeDiffViewerProps {
 | 
				
			||||||
    oldCode: string;
 | 
					    oldCode: string;
 | 
				
			||||||
@ -320,11 +383,17 @@ const ExerciseSheet: React.FC<ExerciseSheetProps> = ({ exercise, onNext, ai }) =
 | 
				
			|||||||
                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
					                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
				
			||||||
                                        placeholder={`// Code für Teil ${part.id}) hier eingeben...`}
 | 
					                                        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
 | 
					                                    <TextAreaEditor
 | 
				
			||||||
                                        value={inputs[part.id] || ''}
 | 
					                                        value={inputs[part.id] || ''}
 | 
				
			||||||
                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
					                                        onChange={(value) => handleInputChange(part.id, value)}
 | 
				
			||||||
                                        placeholder={part.type === 'mermaid' ? 'Antwort in Mermaid-Syntax hier eingeben...' : `Antwort hier eingeben...`}
 | 
					                                        placeholder={`Antwort hier eingeben...`}
 | 
				
			||||||
                                    />
 | 
					                                    />
 | 
				
			||||||
                                )}
 | 
					                                )}
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -375,4 +375,146 @@ CMD [ "serve", "-s", "dist", "-l", "8000" ]
 | 
				
			|||||||
+GEMINI_API_KEY=
 | 
					+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",
 | 
					    "@headlessui/react": "^2.2.4",
 | 
				
			||||||
    "@heroicons/react": "^2.2.0",
 | 
					    "@heroicons/react": "^2.2.0",
 | 
				
			||||||
    "diff": "5.2.0",
 | 
					    "diff": "5.2.0",
 | 
				
			||||||
 | 
					    "mermaid": "^11.8.1",
 | 
				
			||||||
    "prismjs": "1.29.0",
 | 
					    "prismjs": "1.29.0",
 | 
				
			||||||
    "react": "^19.1.0",
 | 
					    "react": "^19.1.0",
 | 
				
			||||||
    "react-dom": "^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