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/AIPromptBar.tsx
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

258 lines
9.3 KiB
TypeScript

'use client'
import { useState, useCallback, useRef, useEffect } from 'react'
import { useTheme } from '@/lib/ThemeContext'
import { useWorksheet } from '@/lib/worksheet-editor/WorksheetContext'
interface AIPromptBarProps {
className?: string
}
export function AIPromptBar({ className = '' }: AIPromptBarProps) {
const { isDark } = useTheme()
const { canvas, saveToHistory } = useWorksheet()
const [prompt, setPrompt] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [lastResult, setLastResult] = useState<string | null>(null)
const [showHistory, setShowHistory] = useState(false)
const [promptHistory, setPromptHistory] = useState<string[]>([])
const inputRef = useRef<HTMLInputElement>(null)
// Load prompt history from localStorage
useEffect(() => {
const stored = localStorage.getItem('worksheet_prompt_history')
if (stored) {
try {
setPromptHistory(JSON.parse(stored))
} catch (e) {
// Ignore parse errors
}
}
}, [])
// Save prompt to history
const addToHistory = (newPrompt: string) => {
const updated = [newPrompt, ...promptHistory.filter(p => p !== newPrompt)].slice(0, 10)
setPromptHistory(updated)
localStorage.setItem('worksheet_prompt_history', JSON.stringify(updated))
}
// Handle AI prompt submission
const handleSubmit = useCallback(async () => {
if (!prompt.trim() || !canvas || isLoading) return
setIsLoading(true)
setLastResult(null)
addToHistory(prompt.trim())
try {
// Get current canvas state
const canvasJSON = JSON.stringify(canvas.toJSON())
// Get API base URL (use same protocol as page)
const { hostname, protocol } = window.location
const apiBase = hostname === 'localhost' ? 'http://localhost:8086' : `${protocol}//${hostname}:8086`
// Send prompt to AI endpoint with timeout (3 minutes for large models)
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 180000) // 3 minutes
try {
const response = await fetch(`${apiBase}/api/v1/worksheet/ai-modify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: prompt.trim(),
canvas_json: canvasJSON,
model: 'qwen2.5vl:32b'
}),
signal: controller.signal
})
clearTimeout(timeoutId)
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
const result = await response.json()
// Apply changes to canvas if we got new JSON
if (result.modified_canvas_json) {
const parsedJSON = JSON.parse(result.modified_canvas_json)
// Preserve current background if not specified in response
const currentBg = canvas.backgroundColor
canvas.loadFromJSON(parsedJSON, () => {
// Ensure white background is set (Fabric.js sometimes loses it)
if (!canvas.backgroundColor || canvas.backgroundColor === 'transparent' || canvas.backgroundColor === '#000000') {
canvas.backgroundColor = parsedJSON.background || currentBg || '#ffffff'
}
canvas.renderAll()
saveToHistory(`AI: ${prompt.trim().substring(0, 30)}`)
})
setLastResult(result.message || 'Aenderungen angewendet')
} else if (result.message) {
setLastResult(result.message)
}
setPrompt('')
} catch (fetchError) {
clearTimeout(timeoutId)
throw fetchError
}
} catch (error) {
console.error('AI prompt error:', error)
if (error instanceof Error && error.name === 'AbortError') {
setLastResult('Zeitüberschreitung - das KI-Modell braucht zu lange. Bitte versuchen Sie es erneut.')
} else {
setLastResult(`Fehler: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`)
}
} finally {
setIsLoading(false)
}
}, [prompt, canvas, isLoading, saveToHistory])
// Handle keyboard shortcuts
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSubmit()
}
if (e.key === 'ArrowUp' && promptHistory.length > 0 && !prompt) {
e.preventDefault()
setPrompt(promptHistory[0])
}
if (e.key === 'Escape') {
setPrompt('')
setShowHistory(false)
inputRef.current?.blur()
}
}
// Example prompts
const examplePrompts = [
'Fuege eine Ueberschrift "Arbeitsblatt" oben hinzu',
'Erstelle ein 3x4 Raster fuer Aufgaben',
'Fuege Linien fuer Schueler-Antworten hinzu',
'Mache alle Texte groesser',
'Zentriere alle Elemente',
'Fuege Nummerierung 1-10 hinzu'
]
// Glassmorphism styles
const barStyle = isDark
? 'backdrop-blur-xl bg-white/10 border border-white/20'
: 'backdrop-blur-xl bg-white/70 border border-black/10 shadow-lg'
const inputStyle = isDark
? 'bg-white/10 border-white/20 text-white placeholder-white/40 focus:border-purple-400 focus:ring-purple-400/30'
: 'bg-white/50 border-black/10 text-slate-900 placeholder-slate-400 focus:border-purple-500 focus:ring-purple-500/30'
const buttonStyle = isDark
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:shadow-lg hover:shadow-purple-500/30'
: 'bg-gradient-to-r from-purple-600 to-pink-600 text-white hover:shadow-lg hover:shadow-purple-600/30'
return (
<div className={`rounded-2xl p-4 ${barStyle} ${className}`}>
{/* Prompt Input */}
<div className="flex items-center gap-3">
<div className="flex-shrink-0">
<span className="text-xl"></span>
</div>
<div className="flex-1 relative">
<input
ref={inputRef}
type="text"
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => setShowHistory(true)}
onBlur={() => setTimeout(() => setShowHistory(false), 200)}
placeholder="Beschreibe, was du aendern moechtest... (z.B. 'Fuege eine Ueberschrift hinzu')"
disabled={isLoading}
className={`w-full px-4 py-3 rounded-xl border text-sm transition-all focus:outline-none focus:ring-2 ${inputStyle} ${
isLoading ? 'opacity-50 cursor-not-allowed' : ''
}`}
/>
{/* History Dropdown */}
{showHistory && promptHistory.length > 0 && !prompt && (
<div className={`absolute top-full left-0 right-0 mt-2 rounded-xl border overflow-hidden z-50 ${
isDark ? 'bg-slate-800 border-white/20' : 'bg-white border-slate-200 shadow-lg'
}`}>
<div className={`px-3 py-2 text-xs font-medium ${isDark ? 'text-white/50' : 'text-slate-400'}`}>
Letzte Prompts
</div>
{promptHistory.map((historyItem, idx) => (
<button
key={idx}
onClick={() => setPrompt(historyItem)}
className={`w-full text-left px-3 py-2 text-sm transition-colors ${
isDark ? 'text-white/80 hover:bg-white/10' : 'text-slate-700 hover:bg-slate-50'
}`}
>
{historyItem}
</button>
))}
</div>
)}
</div>
<button
onClick={handleSubmit}
disabled={isLoading || !prompt.trim()}
className={`flex-shrink-0 px-5 py-3 rounded-xl font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed ${buttonStyle}`}
>
{isLoading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
<span>KI denkt...</span>
</div>
) : (
<div className="flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<span>Anwenden</span>
</div>
)}
</button>
</div>
{/* Result Message */}
{lastResult && (
<div className={`mt-3 px-4 py-2 rounded-lg text-sm ${
lastResult.startsWith('Fehler')
? isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-50 text-red-700'
: isDark ? 'bg-green-500/20 text-green-300' : 'bg-green-50 text-green-700'
}`}>
{lastResult}
</div>
)}
{/* Example Prompts */}
{!prompt && !isLoading && (
<div className="mt-3 flex flex-wrap gap-2">
{examplePrompts.slice(0, 4).map((example, idx) => (
<button
key={idx}
onClick={() => setPrompt(example)}
className={`px-3 py-1 rounded-lg text-xs font-medium transition-colors ${
isDark
? 'bg-white/10 text-white/70 hover:bg-white/20 hover:text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-900'
}`}
>
{example}
</button>
))}
</div>
)}
</div>
)
}