feat: add mermaid diagram support

This commit is contained in:
2025-07-09 13:59:21 +02:00
parent 9ed3520f1f
commit 43a7647a9c
4 changed files with 1215 additions and 2 deletions

73
App.tsx
View File

@ -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>

View File

@ -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.
--- ---

View File

@ -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

File diff suppressed because it is too large Load Diff