Python (6 files in klausur-service): - rbac.py (1,132 → 4), admin_api.py (1,012 → 4) - routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5) Python (2 files in backend-lehrer): - unit_api.py (1,226 → 6), game_api.py (1,129 → 5) Website (6 page files): - 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components in website/components/klausur-korrektur/ (17 shared files) - companion (1,057 → 10), magic-help (1,017 → 8) All re-export barrels preserve backward compatibility. Zero import errors verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
5.3 KiB
TypeScript
140 lines
5.3 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Document Viewer with annotation overlay and page navigation.
|
|
* Left panel (2/3 width) in the Korrektur-Workspace.
|
|
*/
|
|
|
|
import type { Annotation, AnnotationType, AnnotationPosition, StudentWork } from '../../app/admin/klausur-korrektur/types'
|
|
|
|
// Re-use existing annotation components from the klausur-korrektur route
|
|
interface DocumentViewerProps {
|
|
student: StudentWork | null
|
|
documentUrl: string | null
|
|
zoom: number
|
|
currentPage: number
|
|
totalPages: number
|
|
annotations: Annotation[]
|
|
selectedTool: AnnotationType | null
|
|
selectedAnnotation: Annotation | null
|
|
annotationCounts: Record<AnnotationType, number>
|
|
onZoomChange: (zoom: number) => void
|
|
onPageChange: (page: number) => void
|
|
onSelectTool: (tool: AnnotationType | null) => void
|
|
onCreateAnnotation: (position: AnnotationPosition, type: AnnotationType) => void
|
|
onSelectAnnotation: (ann: Annotation) => void
|
|
// Render props for toolbar and annotation layer since they are imported from route-local components
|
|
AnnotationToolbarComponent: React.ComponentType<{
|
|
selectedTool: AnnotationType | null
|
|
onSelectTool: (tool: AnnotationType | null) => void
|
|
zoom: number
|
|
onZoomChange: (zoom: number) => void
|
|
annotationCounts: Record<AnnotationType, number>
|
|
}>
|
|
AnnotationLayerComponent: React.ComponentType<{
|
|
annotations: Annotation[]
|
|
selectedTool: AnnotationType | null
|
|
onCreateAnnotation: (position: AnnotationPosition, type: AnnotationType) => void
|
|
onSelectAnnotation: (ann: Annotation) => void
|
|
selectedAnnotationId?: string
|
|
}>
|
|
}
|
|
|
|
export default function DocumentViewer({
|
|
student, documentUrl, zoom, currentPage, totalPages,
|
|
annotations, selectedTool, selectedAnnotation, annotationCounts,
|
|
onZoomChange, onPageChange, onSelectTool,
|
|
onCreateAnnotation, onSelectAnnotation,
|
|
AnnotationToolbarComponent, AnnotationLayerComponent,
|
|
}: DocumentViewerProps) {
|
|
return (
|
|
<div className="w-2/3 bg-white rounded-lg border border-slate-200 overflow-hidden flex flex-col">
|
|
{/* Toolbar */}
|
|
<AnnotationToolbarComponent
|
|
selectedTool={selectedTool}
|
|
onSelectTool={onSelectTool}
|
|
zoom={zoom}
|
|
onZoomChange={onZoomChange}
|
|
annotationCounts={annotationCounts}
|
|
/>
|
|
|
|
{/* Document display with annotation overlay */}
|
|
<div className="flex-1 overflow-auto p-4 bg-slate-100">
|
|
{documentUrl ? (
|
|
<div
|
|
className="mx-auto bg-white shadow-lg relative"
|
|
style={{ transform: `scale(${zoom / 100})`, transformOrigin: 'top center' }}
|
|
>
|
|
{student?.file_path?.endsWith('.pdf') ? (
|
|
<iframe
|
|
src={documentUrl}
|
|
className="w-full h-[800px] border-0"
|
|
title="Studentenarbeit"
|
|
/>
|
|
) : (
|
|
<div className="relative">
|
|
<img
|
|
src={documentUrl}
|
|
alt="Studentenarbeit"
|
|
className="max-w-full"
|
|
onError={(e) => {
|
|
(e.target as HTMLImageElement).src = '/placeholder-document.png'
|
|
}}
|
|
/>
|
|
<AnnotationLayerComponent
|
|
annotations={annotations.filter((ann) => ann.page === currentPage)}
|
|
selectedTool={selectedTool}
|
|
onCreateAnnotation={onCreateAnnotation}
|
|
onSelectAnnotation={onSelectAnnotation}
|
|
selectedAnnotationId={selectedAnnotation?.id}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center h-full text-slate-400">
|
|
Kein Dokument verfuegbar
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Page navigation */}
|
|
<div className="border-t border-slate-200 p-2 flex items-center justify-center gap-2 bg-slate-50">
|
|
<button
|
|
onClick={() => onPageChange(Math.max(1, currentPage - 1))}
|
|
disabled={currentPage <= 1}
|
|
className="p-1 rounded hover:bg-slate-200 disabled:opacity-50"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<span className="text-sm">
|
|
Seite {currentPage} / {totalPages}
|
|
</span>
|
|
<button
|
|
onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
|
|
disabled={currentPage >= totalPages}
|
|
className="p-1 rounded hover:bg-slate-200 disabled:opacity-50"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* OCR Text (collapsible) */}
|
|
{student?.ocr_text && (
|
|
<details className="border-t border-slate-200">
|
|
<summary className="p-3 bg-slate-50 cursor-pointer text-sm font-medium text-slate-600 hover:bg-slate-100">
|
|
OCR-Text anzeigen
|
|
</summary>
|
|
<div className="p-4 max-h-48 overflow-auto text-sm text-slate-700 bg-slate-50">
|
|
<pre className="whitespace-pre-wrap font-sans">{student.ocr_text}</pre>
|
|
</div>
|
|
</details>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|