Files
breakpilot-lehrer/studio-v2/components/korrektur/AnnotationLayer.tsx
Benjamin Boenisch 5a31f52310 Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:26 +01:00

311 lines
9.5 KiB
TypeScript

'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>
)
}