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:
310
studio-v2/components/korrektur/AnnotationLayer.tsx
Normal file
310
studio-v2/components/korrektur/AnnotationLayer.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useCallback } from 'react'
|
||||
import type { Annotation, AnnotationType, AnnotationPosition } from '@/app/korrektur/types'
|
||||
import { ANNOTATION_COLORS } from '@/app/korrektur/types'
|
||||
|
||||
interface AnnotationLayerProps {
|
||||
annotations: Annotation[]
|
||||
selectedAnnotation: string | null
|
||||
currentTool: AnnotationType | null
|
||||
onAnnotationCreate: (position: AnnotationPosition, type: AnnotationType) => void
|
||||
onAnnotationSelect: (id: string | null) => void
|
||||
onAnnotationDelete: (id: string) => void
|
||||
isEditable?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AnnotationLayer({
|
||||
annotations,
|
||||
selectedAnnotation,
|
||||
currentTool,
|
||||
onAnnotationCreate,
|
||||
onAnnotationSelect,
|
||||
onAnnotationDelete,
|
||||
isEditable = true,
|
||||
className = '',
|
||||
}: AnnotationLayerProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [isDrawing, setIsDrawing] = useState(false)
|
||||
const [drawStart, setDrawStart] = useState<{ x: number; y: number } | null>(null)
|
||||
const [drawEnd, setDrawEnd] = useState<{ x: number; y: number } | null>(null)
|
||||
|
||||
const getRelativePosition = useCallback(
|
||||
(e: React.MouseEvent | MouseEvent): { x: number; y: number } => {
|
||||
if (!containerRef.current) return { x: 0, y: 0 }
|
||||
const rect = containerRef.current.getBoundingClientRect()
|
||||
return {
|
||||
x: ((e.clientX - rect.left) / rect.width) * 100,
|
||||
y: ((e.clientY - rect.top) / rect.height) * 100,
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
if (!isEditable || !currentTool) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const pos = getRelativePosition(e)
|
||||
setIsDrawing(true)
|
||||
setDrawStart(pos)
|
||||
setDrawEnd(pos)
|
||||
onAnnotationSelect(null)
|
||||
}
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isDrawing || !drawStart) return
|
||||
const pos = getRelativePosition(e)
|
||||
setDrawEnd(pos)
|
||||
},
|
||||
[isDrawing, drawStart, getRelativePosition]
|
||||
)
|
||||
|
||||
const handleMouseUp = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isDrawing || !drawStart || !drawEnd || !currentTool) {
|
||||
setIsDrawing(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate rectangle bounds
|
||||
const minX = Math.min(drawStart.x, drawEnd.x)
|
||||
const minY = Math.min(drawStart.y, drawEnd.y)
|
||||
const maxX = Math.max(drawStart.x, drawEnd.x)
|
||||
const maxY = Math.max(drawStart.y, drawEnd.y)
|
||||
|
||||
// Minimum size check (at least 2% width/height)
|
||||
const width = maxX - minX
|
||||
const height = maxY - minY
|
||||
|
||||
if (width >= 1 && height >= 1) {
|
||||
const position: AnnotationPosition = {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: width,
|
||||
height: height,
|
||||
}
|
||||
onAnnotationCreate(position, currentTool)
|
||||
}
|
||||
|
||||
setIsDrawing(false)
|
||||
setDrawStart(null)
|
||||
setDrawEnd(null)
|
||||
},
|
||||
[isDrawing, drawStart, drawEnd, currentTool, onAnnotationCreate]
|
||||
)
|
||||
|
||||
const handleAnnotationClick = (e: React.MouseEvent, id: string) => {
|
||||
e.stopPropagation()
|
||||
onAnnotationSelect(selectedAnnotation === id ? null : id)
|
||||
}
|
||||
|
||||
const handleBackgroundClick = () => {
|
||||
onAnnotationSelect(null)
|
||||
}
|
||||
|
||||
// Drawing preview rectangle
|
||||
const drawingRect =
|
||||
isDrawing && drawStart && drawEnd
|
||||
? {
|
||||
x: Math.min(drawStart.x, drawEnd.x),
|
||||
y: Math.min(drawStart.y, drawEnd.y),
|
||||
width: Math.abs(drawEnd.x - drawStart.x),
|
||||
height: Math.abs(drawEnd.y - drawStart.y),
|
||||
}
|
||||
: null
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`absolute inset-0 ${className}`}
|
||||
style={{
|
||||
pointerEvents: isEditable && currentTool ? 'auto' : 'none',
|
||||
cursor: currentTool ? 'crosshair' : 'default',
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleBackgroundClick}
|
||||
>
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
>
|
||||
{/* Existing Annotations */}
|
||||
{annotations.map((annotation) => {
|
||||
const color = ANNOTATION_COLORS[annotation.type] || '#6b7280'
|
||||
const isSelected = selectedAnnotation === annotation.id
|
||||
|
||||
return (
|
||||
<g
|
||||
key={annotation.id}
|
||||
style={{ pointerEvents: 'auto', cursor: 'pointer' }}
|
||||
onClick={(e) => handleAnnotationClick(e, annotation.id)}
|
||||
>
|
||||
{/* Highlight Rectangle */}
|
||||
<rect
|
||||
x={`${annotation.position.x}%`}
|
||||
y={`${annotation.position.y}%`}
|
||||
width={`${annotation.position.width}%`}
|
||||
height={`${annotation.position.height}%`}
|
||||
fill={`${color}20`}
|
||||
stroke={color}
|
||||
strokeWidth={isSelected ? 3 : 2}
|
||||
strokeDasharray={annotation.type === 'comment' ? '4 2' : 'none'}
|
||||
rx="4"
|
||||
ry="4"
|
||||
className="transition-all"
|
||||
/>
|
||||
|
||||
{/* Type Indicator */}
|
||||
<circle
|
||||
cx={`${annotation.position.x}%`}
|
||||
cy={`${annotation.position.y}%`}
|
||||
r="8"
|
||||
fill={color}
|
||||
className="drop-shadow-sm"
|
||||
/>
|
||||
|
||||
{/* Severity Indicator */}
|
||||
{annotation.severity === 'critical' && (
|
||||
<circle
|
||||
cx={`${annotation.position.x + annotation.position.width}%`}
|
||||
cy={`${annotation.position.y}%`}
|
||||
r="6"
|
||||
fill="#ef4444"
|
||||
className="animate-pulse"
|
||||
/>
|
||||
)}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Drawing Preview */}
|
||||
{drawingRect && currentTool && (
|
||||
<rect
|
||||
x={`${drawingRect.x}%`}
|
||||
y={`${drawingRect.y}%`}
|
||||
width={`${drawingRect.width}%`}
|
||||
height={`${drawingRect.height}%`}
|
||||
fill={`${ANNOTATION_COLORS[currentTool]}30`}
|
||||
stroke={ANNOTATION_COLORS[currentTool]}
|
||||
strokeWidth={2}
|
||||
strokeDasharray="4 2"
|
||||
rx="4"
|
||||
ry="4"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
|
||||
{/* Selected Annotation Popup */}
|
||||
{selectedAnnotation && (
|
||||
<AnnotationPopup
|
||||
annotation={annotations.find((a) => a.id === selectedAnnotation)!}
|
||||
onDelete={() => onAnnotationDelete(selectedAnnotation)}
|
||||
onClose={() => onAnnotationSelect(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ANNOTATION POPUP
|
||||
// =============================================================================
|
||||
|
||||
interface AnnotationPopupProps {
|
||||
annotation: Annotation
|
||||
onDelete: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
function AnnotationPopup({ annotation, onDelete, onClose }: AnnotationPopupProps) {
|
||||
const color = ANNOTATION_COLORS[annotation.type] || '#6b7280'
|
||||
|
||||
const typeLabels: Record<AnnotationType, string> = {
|
||||
rechtschreibung: 'Rechtschreibung',
|
||||
grammatik: 'Grammatik',
|
||||
inhalt: 'Inhalt',
|
||||
struktur: 'Struktur',
|
||||
stil: 'Stil',
|
||||
comment: 'Kommentar',
|
||||
highlight: 'Markierung',
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute z-10 w-64 rounded-xl bg-slate-800/95 backdrop-blur-sm border border-white/10 shadow-xl overflow-hidden"
|
||||
style={{
|
||||
left: `${Math.min(annotation.position.x + annotation.position.width, 70)}%`,
|
||||
top: `${annotation.position.y}%`,
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
className="px-3 py-2 flex items-center justify-between"
|
||||
style={{ backgroundColor: `${color}30` }}
|
||||
>
|
||||
<span className="text-white font-medium text-sm" style={{ color }}>
|
||||
{typeLabels[annotation.type]}
|
||||
</span>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 rounded hover:bg-white/10 text-white/60"
|
||||
>
|
||||
<svg className="w-4 h-4" 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>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-3 space-y-2">
|
||||
{annotation.text && (
|
||||
<p className="text-white/80 text-sm">{annotation.text}</p>
|
||||
)}
|
||||
|
||||
{annotation.suggestion && (
|
||||
<div className="p-2 rounded-lg bg-green-500/10 border border-green-500/20">
|
||||
<p className="text-green-400 text-xs font-medium mb-1">Vorschlag:</p>
|
||||
<p className="text-white/70 text-sm">{annotation.suggestion}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Severity Badge */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
annotation.severity === 'critical'
|
||||
? 'bg-red-500/20 text-red-400'
|
||||
: annotation.severity === 'major'
|
||||
? 'bg-orange-500/20 text-orange-400'
|
||||
: 'bg-gray-500/20 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{annotation.severity === 'critical'
|
||||
? 'Kritisch'
|
||||
: annotation.severity === 'major'
|
||||
? 'Wichtig'
|
||||
: 'Hinweis'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="px-3 py-2 border-t border-white/10 flex gap-2">
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="flex-1 px-3 py-1.5 rounded-lg bg-red-500/20 text-red-400 text-sm hover:bg-red-500/30 transition-colors"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user