Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
481 lines
9.8 KiB
Markdown
481 lines
9.8 KiB
Markdown
# Visual Worksheet Editor - Developer Guide
|
|
|
|
**Version:** 1.0
|
|
**Datum:** 2026-01-23
|
|
|
|
## 1. Schnellstart
|
|
|
|
### 1.1 Dependencies installieren
|
|
|
|
```bash
|
|
cd studio-v2
|
|
npm install fabric@^6.0.0 pdf-lib@^1.17.1
|
|
```
|
|
|
|
### 1.2 Entwicklungsserver starten
|
|
|
|
```bash
|
|
# Frontend (Port 3001)
|
|
cd studio-v2
|
|
npm run dev
|
|
|
|
# Backend (Port 8086)
|
|
cd klausur-service/backend
|
|
source venv/bin/activate
|
|
python main.py
|
|
```
|
|
|
|
### 1.3 Editor öffnen
|
|
|
|
```
|
|
http://localhost:3001/worksheet-editor
|
|
```
|
|
|
|
## 2. Komponenten-Entwicklung
|
|
|
|
### 2.1 Neues Werkzeug hinzufügen
|
|
|
|
1. **Tool-Typ definieren** in `types.ts`:
|
|
|
|
```typescript
|
|
export type EditorTool =
|
|
| 'select'
|
|
| 'text'
|
|
// ... existierende Tools
|
|
| 'neues-tool' // NEU
|
|
```
|
|
|
|
2. **Button hinzufügen** in `EditorToolbar.tsx`:
|
|
|
|
```tsx
|
|
<ToolButton
|
|
tool="neues-tool"
|
|
isActive={activeTool === 'neues-tool'}
|
|
onClick={() => handleToolClick('neues-tool')}
|
|
isDark={isDark}
|
|
label="Neues Tool"
|
|
icon={<svg>...</svg>}
|
|
/>
|
|
```
|
|
|
|
3. **Handler implementieren** in `FabricCanvas.tsx`:
|
|
|
|
```typescript
|
|
case 'neues-tool': {
|
|
// Canvas-Objekt erstellen
|
|
const obj = new fabric.CustomObject({...})
|
|
fabricCanvas.add(obj)
|
|
fabricCanvas.setActiveObject(obj)
|
|
setActiveTool('select')
|
|
break
|
|
}
|
|
```
|
|
|
|
### 2.2 Eigenschaften-Panel erweitern
|
|
|
|
In `PropertiesPanel.tsx` neue Eigenschaften für einen Objekttyp hinzufügen:
|
|
|
|
```tsx
|
|
{isMyType && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>
|
|
Neue Eigenschaft
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={myProperty}
|
|
onChange={(e) => {
|
|
setMyProperty(e.target.value)
|
|
updateProperty('myProperty', e.target.value)
|
|
}}
|
|
className={`w-full px-3 py-2 rounded-xl border text-sm ${inputStyle}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
```
|
|
|
|
## 3. Context API
|
|
|
|
### 3.1 State abrufen
|
|
|
|
```tsx
|
|
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
|
|
|
|
function MyComponent() {
|
|
const {
|
|
canvas,
|
|
activeTool,
|
|
setActiveTool,
|
|
selectedObjects,
|
|
zoom,
|
|
setZoom
|
|
} = useWorksheet()
|
|
|
|
// Verwenden...
|
|
}
|
|
```
|
|
|
|
### 3.2 Canvas-Operationen
|
|
|
|
```typescript
|
|
// Objekt hinzufügen
|
|
canvas.add(newObject)
|
|
canvas.setActiveObject(newObject)
|
|
canvas.renderAll()
|
|
|
|
// Objekt entfernen
|
|
canvas.remove(selectedObject)
|
|
|
|
// Alle Objekte abrufen (ohne Grid)
|
|
const objects = canvas.getObjects().filter(obj => !obj.isGrid)
|
|
|
|
// Canvas exportieren
|
|
const json = canvas.toJSON()
|
|
const dataUrl = canvas.toDataURL({ format: 'png', multiplier: 2 })
|
|
|
|
// Canvas laden
|
|
canvas.loadFromJSON(jsonData, () => {
|
|
canvas.renderAll()
|
|
})
|
|
```
|
|
|
|
## 4. API-Integration
|
|
|
|
### 4.1 Worksheet speichern
|
|
|
|
```typescript
|
|
const saveWorksheet = async () => {
|
|
const host = window.location.hostname
|
|
const apiBase = `http://${host}:8086`
|
|
|
|
const response = await fetch(`${apiBase}/api/v1/worksheet/save`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
id: worksheetId,
|
|
title: 'Mein Arbeitsblatt',
|
|
pages: [{
|
|
id: 'page_1',
|
|
index: 0,
|
|
canvasJSON: JSON.stringify(canvas.toJSON())
|
|
}]
|
|
})
|
|
})
|
|
|
|
const result = await response.json()
|
|
console.log('Gespeichert:', result.id)
|
|
}
|
|
```
|
|
|
|
### 4.2 KI-Bild generieren
|
|
|
|
```typescript
|
|
const generateAIImage = async (prompt: string) => {
|
|
const host = window.location.hostname
|
|
const apiBase = `http://${host}:8086`
|
|
|
|
const response = await fetch(`${apiBase}/api/v1/worksheet/ai-image`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
prompt,
|
|
style: 'educational',
|
|
width: 512,
|
|
height: 512
|
|
})
|
|
})
|
|
|
|
const { image_base64, error } = await response.json()
|
|
|
|
if (image_base64) {
|
|
// Bild zum Canvas hinzufügen
|
|
fabric.Image.fromURL(image_base64, (img) => {
|
|
canvas.add(img)
|
|
canvas.setActiveObject(img)
|
|
canvas.renderAll()
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
## 5. Fabric.js Patterns
|
|
|
|
### 5.1 Text-Objekt erstellen
|
|
|
|
```typescript
|
|
const text = new fabric.IText('Text eingeben', {
|
|
left: 100,
|
|
top: 100,
|
|
fontFamily: 'Arial',
|
|
fontSize: 16,
|
|
fill: '#000000',
|
|
})
|
|
canvas.add(text)
|
|
text.enterEditing() // Bearbeitungsmodus
|
|
```
|
|
|
|
### 5.2 Form erstellen
|
|
|
|
```typescript
|
|
// Rechteck
|
|
const rect = new fabric.Rect({
|
|
left: 100,
|
|
top: 100,
|
|
width: 200,
|
|
height: 100,
|
|
fill: 'transparent',
|
|
stroke: '#000000',
|
|
strokeWidth: 2,
|
|
rx: 5, // Eckenradius
|
|
ry: 5,
|
|
})
|
|
|
|
// Kreis
|
|
const circle = new fabric.Circle({
|
|
left: 100,
|
|
top: 100,
|
|
radius: 50,
|
|
fill: '#ff6b6b',
|
|
stroke: '#000000',
|
|
strokeWidth: 2,
|
|
})
|
|
|
|
// Linie
|
|
const line = new fabric.Line([50, 50, 200, 50], {
|
|
stroke: '#000000',
|
|
strokeWidth: 2,
|
|
})
|
|
```
|
|
|
|
### 5.3 Bild laden
|
|
|
|
```typescript
|
|
fabric.Image.fromURL(imageUrl, (img) => {
|
|
// Skalierung auf max. Größe
|
|
const maxWidth = 400
|
|
const maxHeight = 300
|
|
const scale = Math.min(maxWidth / img.width, maxHeight / img.height, 1)
|
|
|
|
img.set({
|
|
left: 100,
|
|
top: 100,
|
|
scaleX: scale,
|
|
scaleY: scale,
|
|
})
|
|
|
|
canvas.add(img)
|
|
}, { crossOrigin: 'anonymous' })
|
|
```
|
|
|
|
### 5.4 Events
|
|
|
|
```typescript
|
|
// Selection Events
|
|
canvas.on('selection:created', (e) => {
|
|
const selected = canvas.getActiveObjects()
|
|
console.log('Ausgewählt:', selected.length)
|
|
})
|
|
|
|
canvas.on('selection:cleared', () => {
|
|
console.log('Auswahl aufgehoben')
|
|
})
|
|
|
|
// Object Events
|
|
canvas.on('object:modified', (e) => {
|
|
console.log('Objekt geändert:', e.target)
|
|
saveToHistory('modified')
|
|
})
|
|
|
|
canvas.on('object:added', (e) => {
|
|
console.log('Objekt hinzugefügt:', e.target)
|
|
})
|
|
|
|
// Mouse Events
|
|
canvas.on('mouse:down', (e) => {
|
|
const pointer = canvas.getPointer(e.e)
|
|
console.log('Klick bei:', pointer.x, pointer.y)
|
|
})
|
|
```
|
|
|
|
## 6. Testing
|
|
|
|
### 6.1 Unit Tests für Context
|
|
|
|
```typescript
|
|
// __tests__/worksheet-context.test.tsx
|
|
import { renderHook, act } from '@testing-library/react'
|
|
import { WorksheetProvider, useWorksheet } from '../WorksheetContext'
|
|
|
|
describe('useWorksheet', () => {
|
|
it('should initialize with default values', () => {
|
|
const { result } = renderHook(() => useWorksheet(), {
|
|
wrapper: WorksheetProvider,
|
|
})
|
|
|
|
expect(result.current.activeTool).toBe('select')
|
|
expect(result.current.zoom).toBe(1)
|
|
expect(result.current.showGrid).toBe(true)
|
|
})
|
|
|
|
it('should change tool', () => {
|
|
const { result } = renderHook(() => useWorksheet(), {
|
|
wrapper: WorksheetProvider,
|
|
})
|
|
|
|
act(() => {
|
|
result.current.setActiveTool('text')
|
|
})
|
|
|
|
expect(result.current.activeTool).toBe('text')
|
|
})
|
|
})
|
|
```
|
|
|
|
### 6.2 API Tests
|
|
|
|
```python
|
|
# tests/test_worksheet_editor_api.py
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from main import app
|
|
|
|
client = TestClient(app)
|
|
|
|
def test_save_worksheet():
|
|
response = client.post("/api/v1/worksheet/save", json={
|
|
"title": "Test Worksheet",
|
|
"pages": [{
|
|
"id": "page_1",
|
|
"index": 0,
|
|
"canvasJSON": "{}"
|
|
}]
|
|
})
|
|
assert response.status_code == 200
|
|
assert "id" in response.json()
|
|
|
|
def test_get_worksheet():
|
|
# Erst erstellen
|
|
create_response = client.post("/api/v1/worksheet/save", json={
|
|
"title": "Test",
|
|
"pages": [{"id": "p1", "index": 0, "canvasJSON": "{}"}]
|
|
})
|
|
worksheet_id = create_response.json()["id"]
|
|
|
|
# Dann laden
|
|
response = client.get(f"/api/v1/worksheet/{worksheet_id}")
|
|
assert response.status_code == 200
|
|
assert response.json()["title"] == "Test"
|
|
|
|
def test_ai_image_generation():
|
|
response = client.post("/api/v1/worksheet/ai-image", json={
|
|
"prompt": "A friendly dog",
|
|
"style": "cartoon",
|
|
"width": 256,
|
|
"height": 256
|
|
})
|
|
# Kann 200 (Bild) oder 503 (Ollama nicht verfügbar) sein
|
|
assert response.status_code in [200, 503]
|
|
```
|
|
|
|
## 7. Styling
|
|
|
|
### 7.1 Glassmorphism Utilities
|
|
|
|
```typescript
|
|
// Für Theme-aware Styling
|
|
const glassCard = isDark
|
|
? 'backdrop-blur-xl bg-white/10 border border-white/20'
|
|
: 'backdrop-blur-xl bg-white/70 border border-black/10 shadow-xl'
|
|
|
|
const glassInput = isDark
|
|
? 'bg-white/10 border-white/20 text-white placeholder-white/40'
|
|
: 'bg-white/50 border-black/10 text-slate-900 placeholder-slate-400'
|
|
|
|
const labelStyle = isDark ? 'text-white/70' : 'text-slate-600'
|
|
```
|
|
|
|
### 7.2 Button States
|
|
|
|
```typescript
|
|
const buttonStyle = (active: boolean) => isDark
|
|
? active
|
|
? 'bg-purple-500/30 text-purple-300'
|
|
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
|
: active
|
|
? 'bg-purple-100 text-purple-700'
|
|
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900'
|
|
```
|
|
|
|
## 8. Best Practices
|
|
|
|
### 8.1 Performance
|
|
|
|
- Grid-Objekte mit `isGrid: true` markieren und vom Export ausschließen
|
|
- Canvas-JSON nur bei tatsächlichen Änderungen speichern
|
|
- History auf 50 Einträge limitieren
|
|
- Bilder mit `multiplier: 2` für Retina-Export
|
|
|
|
### 8.2 Fehlerbehandlung
|
|
|
|
```typescript
|
|
try {
|
|
const response = await fetch(apiUrl)
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP ${response.status}`)
|
|
}
|
|
const data = await response.json()
|
|
return data
|
|
} catch (error) {
|
|
console.error('API Error:', error)
|
|
// Fallback auf localStorage
|
|
return JSON.parse(localStorage.getItem(key) || '{}')
|
|
}
|
|
```
|
|
|
|
### 8.3 Hydration Safety
|
|
|
|
```typescript
|
|
const [mounted, setMounted] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setMounted(true)
|
|
}, [])
|
|
|
|
if (!mounted) {
|
|
return <LoadingSpinner />
|
|
}
|
|
```
|
|
|
|
## 9. Debugging
|
|
|
|
### 9.1 Canvas-State inspizieren
|
|
|
|
```typescript
|
|
// Im Browser DevTools Console
|
|
const canvas = document.querySelector('canvas')?.__fabric
|
|
console.log('Objects:', canvas?.getObjects())
|
|
console.log('Active:', canvas?.getActiveObject())
|
|
console.log('JSON:', canvas?.toJSON())
|
|
```
|
|
|
|
### 9.2 API-Calls überwachen
|
|
|
|
```bash
|
|
# Backend-Logs
|
|
tail -f /var/log/klausur-service.log
|
|
|
|
# Oder im Terminal wo main.py läuft
|
|
```
|
|
|
|
## 10. Deployment Checklist
|
|
|
|
- [ ] Dependencies in package.json
|
|
- [ ] Fabric.js und pdf-lib installiert
|
|
- [ ] Backend-Router registriert
|
|
- [ ] i18n-Übersetzungen vorhanden
|
|
- [ ] Sidebar-Navigation aktualisiert
|
|
- [ ] CORS für Produktions-Domain konfiguriert
|
|
- [ ] Ollama-URL in Umgebungsvariablen
|