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>
311 lines
9.5 KiB
TypeScript
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>
|
|
)
|
|
}
|