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