fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
480
klausur-service/docs/Worksheet-Editor-Developer-Guide.md
Normal file
480
klausur-service/docs/Worksheet-Editor-Developer-Guide.md
Normal file
@@ -0,0 +1,480 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user