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/studio-v2/components/worksheet-editor/AIImageGenerator.tsx
Benjamin Admin 21a844cb8a 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

297 lines
11 KiB
TypeScript

'use client'
import { useState } from 'react'
import { useTheme } from '@/lib/ThemeContext'
import { useLanguage } from '@/lib/LanguageContext'
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
import type { AIImageStyle } from '@/app/worksheet-editor/types'
interface AIImageGeneratorProps {
isOpen: boolean
onClose: () => void
}
const AI_STYLES: { id: AIImageStyle; name: string; description: string }[] = [
{ id: 'educational', name: 'Bildung', description: 'Klare, lehrreiche Illustrationen' },
{ id: 'cartoon', name: 'Cartoon', description: 'Bunte, kindgerechte Zeichnungen' },
{ id: 'realistic', name: 'Realistisch', description: 'Fotorealistische Darstellungen' },
{ id: 'sketch', name: 'Skizze', description: 'Handgezeichneter Stil' },
{ id: 'clipart', name: 'Clipart', description: 'Einfache, flache Grafiken' },
]
const SIZE_OPTIONS = [
{ width: 256, height: 256, label: '256 x 256' },
{ width: 512, height: 512, label: '512 x 512' },
{ width: 512, height: 256, label: '512 x 256 (Breit)' },
{ width: 256, height: 512, label: '256 x 512 (Hoch)' },
]
export function AIImageGenerator({ isOpen, onClose }: AIImageGeneratorProps) {
const { isDark } = useTheme()
const { t } = useLanguage()
const { canvas, setActiveTool, saveToHistory } = useWorksheet()
const [prompt, setPrompt] = useState('')
const [style, setStyle] = useState<AIImageStyle>('educational')
const [sizeIndex, setSizeIndex] = useState(1) // Default: 512x512
const [isGenerating, setIsGenerating] = useState(false)
const [previewUrl, setPreviewUrl] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const selectedSize = SIZE_OPTIONS[sizeIndex]
const handleGenerate = async () => {
if (!prompt.trim()) {
setError('Bitte geben Sie eine Beschreibung ein.')
return
}
setIsGenerating(true)
setError(null)
setPreviewUrl(null)
const { hostname, protocol } = window.location
const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
try {
const response = await fetch(`${apiBase}/api/v1/worksheet/ai-image`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt,
style,
width: selectedSize.width,
height: selectedSize.height
})
})
if (!response.ok) {
const data = await response.json()
throw new Error(data.detail || 'Bildgenerierung fehlgeschlagen')
}
const data = await response.json()
if (data.error) {
throw new Error(data.error)
}
setPreviewUrl(data.image_base64)
} catch (err: any) {
console.error('AI Image generation failed:', err)
setError(err.message || 'Verbindung zum KI-Server fehlgeschlagen. Bitte überprüfen Sie, ob Ollama läuft.')
} finally {
setIsGenerating(false)
}
}
const handleInsert = () => {
if (!previewUrl || !canvas) return
// Add image to canvas
if ((canvas as any).addImage) {
(canvas as any).addImage(previewUrl)
}
// Reset and close
setPrompt('')
setPreviewUrl(null)
setActiveTool('select')
onClose()
}
if (!isOpen) return null
// Glassmorphism styles
const overlayStyle = 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50'
const modalStyle = isDark
? 'backdrop-blur-xl bg-white/10 border border-white/20'
: 'backdrop-blur-xl bg-white/90 border border-black/10 shadow-2xl'
const inputStyle = isDark
? 'bg-white/10 border-white/20 text-white placeholder-white/40 focus:border-purple-400'
: 'bg-white/50 border-black/10 text-slate-900 placeholder-slate-400 focus:border-purple-500'
const labelStyle = isDark ? 'text-white/70' : 'text-slate-600'
return (
<div className={overlayStyle} onClick={onClose}>
<div
className={`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-xl rounded-3xl p-6 ${modalStyle}`}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${
isDark ? 'bg-purple-500/20' : 'bg-purple-100'
}`}>
<svg className={`w-6 h-6 ${isDark ? 'text-purple-300' : 'text-purple-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
</div>
<div>
<h2 className={`text-xl font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
KI-Bild generieren
</h2>
<p className={`text-sm ${labelStyle}`}>
Beschreiben Sie das gewünschte Bild
</p>
</div>
</div>
<button
onClick={onClose}
className={`p-2 rounded-xl transition-colors ${
isDark ? 'hover:bg-white/10 text-white/70' : 'hover:bg-slate-100 text-slate-500'
}`}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Prompt Input */}
<div className="mb-4">
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>
Beschreibung
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="z.B. Ein freundlicher Cartoon-Hund, der ein Buch liest"
rows={3}
className={`w-full px-4 py-3 rounded-xl border resize-none focus:outline-none focus:ring-2 focus:ring-purple-500/50 ${inputStyle}`}
/>
</div>
{/* Style Selection */}
<div className="mb-4">
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>
Stil
</label>
<div className="grid grid-cols-5 gap-2">
{AI_STYLES.map((s) => (
<button
key={s.id}
onClick={() => setStyle(s.id)}
className={`p-3 rounded-xl text-center transition-all ${
style === s.id
? isDark
? 'bg-purple-500/30 text-purple-300 border border-purple-400/50'
: 'bg-purple-100 text-purple-700 border border-purple-300'
: isDark
? 'bg-white/5 text-white/70 border border-transparent hover:bg-white/10'
: 'bg-slate-50 text-slate-600 border border-transparent hover:bg-slate-100'
}`}
>
<span className="text-xs font-medium">{s.name}</span>
</button>
))}
</div>
</div>
{/* Size Selection */}
<div className="mb-6">
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>
Größe
</label>
<div className="grid grid-cols-4 gap-2">
{SIZE_OPTIONS.map((size, index) => (
<button
key={size.label}
onClick={() => setSizeIndex(index)}
className={`py-2 px-3 rounded-xl text-xs font-medium transition-all ${
sizeIndex === index
? isDark
? 'bg-purple-500/30 text-purple-300 border border-purple-400/50'
: 'bg-purple-100 text-purple-700 border border-purple-300'
: isDark
? 'bg-white/5 text-white/70 border border-transparent hover:bg-white/10'
: 'bg-slate-50 text-slate-600 border border-transparent hover:bg-slate-100'
}`}
>
{size.label}
</button>
))}
</div>
</div>
{/* Error Message */}
{error && (
<div className={`mb-4 p-3 rounded-xl ${
isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-50 text-red-600'
}`}>
<p className="text-sm">{error}</p>
</div>
)}
{/* Preview */}
{previewUrl && (
<div className="mb-6">
<label className={`block text-sm font-medium mb-2 ${labelStyle}`}>
Vorschau
</label>
<div className={`rounded-xl overflow-hidden ${
isDark ? 'bg-white/5' : 'bg-slate-100'
}`}>
<img
src={previewUrl}
alt="Generated preview"
className="w-full h-auto"
/>
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-3">
<button
onClick={handleGenerate}
disabled={isGenerating || !prompt.trim()}
className={`flex-1 py-3 rounded-xl font-medium transition-all flex items-center justify-center gap-2 ${
isGenerating || !prompt.trim()
? isDark
? 'bg-white/10 text-white/30 cursor-not-allowed'
: 'bg-slate-100 text-slate-400 cursor-not-allowed'
: isDark
? 'bg-purple-500/30 text-purple-300 hover:bg-purple-500/40'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{isGenerating ? (
<>
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Generiere...
</>
) : (
<>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
Generieren
</>
)}
</button>
{previewUrl && (
<button
onClick={handleInsert}
className={`flex-1 py-3 rounded-xl font-medium transition-all ${
isDark
? 'bg-green-500/30 text-green-300 hover:bg-green-500/40'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
Einfügen
</button>
)}
</div>
</div>
</div>
)
}