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:
257
studio-v2/components/worksheet-editor/AIPromptBar.tsx
Normal file
257
studio-v2/components/worksheet-editor/AIPromptBar.tsx
Normal file
@@ -0,0 +1,257 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user