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:
194
studio-v2/components/korrektur/GutachtenEditor.tsx
Normal file
194
studio-v2/components/korrektur/GutachtenEditor.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
interface GutachtenEditorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onGenerate?: () => void
|
||||
isGenerating?: boolean
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GutachtenEditor({
|
||||
value,
|
||||
onChange,
|
||||
onGenerate,
|
||||
isGenerating = false,
|
||||
placeholder = 'Gutachten hier eingeben oder generieren lassen...',
|
||||
className = '',
|
||||
}: GutachtenEditorProps) {
|
||||
const [isFocused, setIsFocused] = useState(false)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Auto-resize textarea
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto'
|
||||
textarea.style.height = `${Math.max(200, textarea.scrollHeight)}px`
|
||||
}
|
||||
}, [value])
|
||||
|
||||
// Word count
|
||||
const wordCount = value.trim() ? value.trim().split(/\s+/).length : 0
|
||||
const charCount = value.length
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-white font-semibold">Gutachten</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-white/40 text-xs">
|
||||
{wordCount} Woerter / {charCount} Zeichen
|
||||
</span>
|
||||
{onGenerate && (
|
||||
<button
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating}
|
||||
className="px-3 py-1.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-500 text-white text-sm font-medium hover:shadow-lg hover:shadow-purple-500/30 transition-all disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
Generiere...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<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>
|
||||
KI Generieren
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor */}
|
||||
<div
|
||||
className={`relative rounded-2xl transition-all ${
|
||||
isFocused
|
||||
? 'ring-2 ring-purple-500 ring-offset-2 ring-offset-slate-900'
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
className="w-full min-h-[200px] p-4 rounded-2xl bg-white/5 border border-white/10 text-white placeholder-white/30 resize-none focus:outline-none"
|
||||
/>
|
||||
|
||||
{/* Loading Overlay */}
|
||||
{isGenerating && (
|
||||
<div className="absolute inset-0 bg-slate-900/80 backdrop-blur-sm rounded-2xl flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mx-auto mb-3" />
|
||||
<p className="text-white/60 text-sm">Gutachten wird generiert...</p>
|
||||
<p className="text-white/40 text-xs mt-1">Nutzt 500+ NiBiS Dokumente</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Insert Buttons */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<QuickInsertButton
|
||||
label="Einleitung"
|
||||
onClick={() => onChange(value + '\n\nEinleitung:\n')}
|
||||
/>
|
||||
<QuickInsertButton
|
||||
label="Staerken"
|
||||
onClick={() => onChange(value + '\n\nStaerken der Arbeit:\n- ')}
|
||||
/>
|
||||
<QuickInsertButton
|
||||
label="Schwaechen"
|
||||
onClick={() => onChange(value + '\n\nVerbesserungsmoeglichkeiten:\n- ')}
|
||||
/>
|
||||
<QuickInsertButton
|
||||
label="Fazit"
|
||||
onClick={() => onChange(value + '\n\nGesamteindruck:\n')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// QUICK INSERT BUTTON
|
||||
// =============================================================================
|
||||
|
||||
interface QuickInsertButtonProps {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
function QuickInsertButton({ label, onClick }: QuickInsertButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-3 py-1 rounded-lg bg-white/5 border border-white/10 text-white/60 text-xs hover:bg-white/10 hover:text-white transition-colors"
|
||||
>
|
||||
+ {label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// GUTACHTEN PREVIEW (Read-only)
|
||||
// =============================================================================
|
||||
|
||||
interface GutachtenPreviewProps {
|
||||
value: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function GutachtenPreview({ value, className = '' }: GutachtenPreviewProps) {
|
||||
if (!value) {
|
||||
return (
|
||||
<div className={`p-4 rounded-2xl bg-white/5 border border-white/10 text-white/40 text-center ${className}`}>
|
||||
Kein Gutachten vorhanden
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Split into paragraphs for better rendering
|
||||
const paragraphs = value.split('\n\n').filter(Boolean)
|
||||
|
||||
return (
|
||||
<div className={`p-4 rounded-2xl bg-white/5 border border-white/10 space-y-4 ${className}`}>
|
||||
{paragraphs.map((paragraph, index) => {
|
||||
// Check if it's a heading (ends with :)
|
||||
const lines = paragraph.split('\n')
|
||||
const firstLine = lines[0]
|
||||
const isHeading = firstLine.endsWith(':')
|
||||
|
||||
if (isHeading) {
|
||||
return (
|
||||
<div key={index}>
|
||||
<h4 className="text-white font-semibold mb-2">{firstLine}</h4>
|
||||
{lines.slice(1).map((line, lineIndex) => (
|
||||
<p key={lineIndex} className="text-white/70 text-sm">
|
||||
{line}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p key={index} className="text-white/70 text-sm whitespace-pre-wrap">
|
||||
{paragraph}
|
||||
</p>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user