This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/klausur-service/docs/Worksheet-Editor-Developer-Guide.md
Benjamin Admin bfdaf63ba9 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>
2026-02-09 09:51:32 +01:00

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

  1. Tool-Typ definieren in types.ts:
export type EditorTool =
  | 'select'
  | 'text'
  // ... existierende Tools
  | 'neues-tool'  // NEU
  1. 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>}
/>
  1. 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: 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

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