A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.8 KiB
9.8 KiB
Visual Worksheet Editor - Developer Guide
Version: 1.0 Datum: 2026-01-23
1. Schnellstart
1.1 Dependencies installieren
cd studio-v2
npm install fabric@^6.0.0 pdf-lib@^1.17.1
1.2 Entwicklungsserver starten
# 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
- Tool-Typ definieren in
types.ts:
export type EditorTool =
| 'select'
| 'text'
// ... existierende Tools
| 'neues-tool' // NEU
- Button hinzufügen in
EditorToolbar.tsx:
<ToolButton
tool="neues-tool"
isActive={activeTool === 'neues-tool'}
onClick={() => handleToolClick('neues-tool')}
isDark={isDark}
label="Neues Tool"
icon={<svg>...</svg>}
/>
- Handler implementieren in
FabricCanvas.tsx:
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:
{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
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
function MyComponent() {
const {
canvas,
activeTool,
setActiveTool,
selectedObjects,
zoom,
setZoom
} = useWorksheet()
// Verwenden...
}
3.2 Canvas-Operationen
// 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
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
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
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
// 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
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
// 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
// __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
# 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
// 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
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: truemarkieren und vom Export ausschließen - Canvas-JSON nur bei tatsächlichen Änderungen speichern
- History auf 50 Einträge limitieren
- Bilder mit
multiplier: 2für Retina-Export
8.2 Fehlerbehandlung
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
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <LoadingSpinner />
}
9. Debugging
9.1 Canvas-State inspizieren
// 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
# 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