[split-required] Split final batch of monoliths >1000 LOC
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>
This commit is contained in:
285
website/components/klausur-korrektur/DirektuploadTab.tsx
Normal file
285
website/components/klausur-korrektur/DirektuploadTab.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Direct upload wizard tab (3 steps).
|
||||
* Allows quick upload of student work files without creating a klausur first.
|
||||
*/
|
||||
|
||||
import type { DirektuploadForm, TabId } from './list-types'
|
||||
|
||||
interface DirektuploadTabProps {
|
||||
direktForm: DirektuploadForm
|
||||
direktStep: 1 | 2 | 3
|
||||
uploading: boolean
|
||||
onFormChange: (form: DirektuploadForm) => void
|
||||
onStepChange: (step: 1 | 2 | 3) => void
|
||||
onUpload: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export default function DirektuploadTab({
|
||||
direktForm, direktStep, uploading,
|
||||
onFormChange, onStepChange, onUpload, onCancel,
|
||||
}: DirektuploadTabProps) {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
{/* Progress Header */}
|
||||
<div className="bg-slate-50 border-b border-slate-200 px-6 py-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h2 className="text-lg font-semibold text-slate-800">Schnellstart - Direkt Korrigieren</h2>
|
||||
<button onClick={onCancel} className="text-sm text-slate-500 hover:text-slate-700">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{[1, 2, 3].map((step) => (
|
||||
<div key={step} className="flex items-center gap-2 flex-1">
|
||||
<div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium ${
|
||||
direktStep >= step ? 'bg-blue-600 text-white' : 'bg-slate-200 text-slate-500'
|
||||
}`}>
|
||||
{step}
|
||||
</div>
|
||||
<span className={`text-sm ${direktStep >= step ? 'text-slate-800' : 'text-slate-400'}`}>
|
||||
{step === 1 ? 'Arbeiten' : step === 2 ? 'Erwartungshorizont' : 'Starten'}
|
||||
</span>
|
||||
{step < 3 && <div className={`flex-1 h-1 rounded ${direktStep > step ? 'bg-blue-600' : 'bg-slate-200'}`} />}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{/* Step 1: Upload Files */}
|
||||
{direktStep === 1 && (
|
||||
<Step1Files
|
||||
files={direktForm.files}
|
||||
onFilesChange={(files) => onFormChange({ ...direktForm, files })}
|
||||
onNext={() => onStepChange(2)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step 2: EH */}
|
||||
{direktStep === 2 && (
|
||||
<Step2EH
|
||||
aufgabentyp={direktForm.aufgabentyp}
|
||||
ehText={direktForm.ehText}
|
||||
onAufgabentypChange={(v) => onFormChange({ ...direktForm, aufgabentyp: v })}
|
||||
onEhTextChange={(v) => onFormChange({ ...direktForm, ehText: v })}
|
||||
onBack={() => onStepChange(1)}
|
||||
onNext={() => onStepChange(3)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step 3: Confirm */}
|
||||
{direktStep === 3 && (
|
||||
<Step3Confirm
|
||||
direktForm={direktForm}
|
||||
uploading={uploading}
|
||||
onTitleChange={(v) => onFormChange({ ...direktForm, klausurTitle: v })}
|
||||
onBack={() => onStepChange(2)}
|
||||
onUpload={onUpload}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// --- Sub-components for each step ---
|
||||
|
||||
function Step1Files({ files, onFilesChange, onNext }: {
|
||||
files: File[]; onFilesChange: (f: File[]) => void; onNext: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-800 mb-2">Schuelerarbeiten hochladen</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Laden Sie die eingescannten Klausuren hoch. Unterstuetzte Formate: PDF, JPG, PNG.
|
||||
</p>
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-xl p-8 text-center transition-colors ${
|
||||
files.length > 0 ? 'border-green-300 bg-green-50' : 'border-slate-300 hover:border-blue-400 hover:bg-blue-50'
|
||||
}`}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault()
|
||||
onFilesChange([...files, ...Array.from(e.dataTransfer.files)])
|
||||
}}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
>
|
||||
<svg className="w-12 h-12 mx-auto text-slate-400 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
|
||||
</svg>
|
||||
<p className="text-slate-600 mb-2">Dateien hier ablegen oder</p>
|
||||
<label className="inline-block px-4 py-2 bg-blue-600 text-white rounded-lg cursor-pointer hover:bg-blue-700">
|
||||
Dateien auswaehlen
|
||||
<input type="file" multiple accept=".pdf,.jpg,.jpeg,.png" className="hidden"
|
||||
onChange={(e) => onFilesChange([...files, ...Array.from(e.target.files || [])])}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center justify-between text-sm text-slate-600">
|
||||
<span>{files.length} Datei{files.length !== 1 ? 'en' : ''} ausgewaehlt</span>
|
||||
<button onClick={() => onFilesChange([])} className="text-red-600 hover:text-red-700">Alle entfernen</button>
|
||||
</div>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1">
|
||||
{files.map((file, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between bg-slate-50 px-3 py-2 rounded-lg text-sm">
|
||||
<span className="truncate">{file.name}</span>
|
||||
<button
|
||||
onClick={() => onFilesChange(files.filter((_, i) => i !== idx))}
|
||||
className="text-slate-400 hover:text-red-600"
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onNext}
|
||||
disabled={files.length === 0}
|
||||
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step2EH({ aufgabentyp, ehText, onAufgabentypChange, onEhTextChange, onBack, onNext }: {
|
||||
aufgabentyp: string; ehText: string
|
||||
onAufgabentypChange: (v: string) => void; onEhTextChange: (v: string) => void
|
||||
onBack: () => void; onNext: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-800 mb-2">Erwartungshorizont (optional)</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Laden Sie Ihren eigenen Erwartungshorizont hoch oder beschreiben Sie die Aufgabenstellung.
|
||||
Dies hilft der KI, passendere Bewertungen vorzuschlagen.
|
||||
</p>
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Aufgabentyp</label>
|
||||
<select
|
||||
value={aufgabentyp}
|
||||
onChange={(e) => onAufgabentypChange(e.target.value)}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
>
|
||||
<option value="">-- Waehlen Sie einen Aufgabentyp --</option>
|
||||
<option value="textanalyse_pragmatisch">Textanalyse (Sachtexte)</option>
|
||||
<option value="gedichtanalyse">Gedichtanalyse</option>
|
||||
<option value="prosaanalyse">Prosaanalyse</option>
|
||||
<option value="dramenanalyse">Dramenanalyse</option>
|
||||
<option value="eroerterung_textgebunden">Textgebundene Eroerterung</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Aufgabenstellung / Erwartungshorizont</label>
|
||||
<textarea
|
||||
value={ehText}
|
||||
onChange={(e) => onEhTextChange(e.target.value)}
|
||||
placeholder="Beschreiben Sie hier die Aufgabenstellung und Ihre Erwartungen an eine gute Loesung..."
|
||||
rows={6}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Je detaillierter Sie die Erwartungen beschreiben, desto besser werden die KI-Vorschlaege.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button onClick={onBack} className="px-6 py-2 text-slate-600 hover:bg-slate-100 rounded-lg">Zurueck</button>
|
||||
<button onClick={onNext} className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">Weiter</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step3Confirm({ direktForm, uploading, onTitleChange, onBack, onUpload }: {
|
||||
direktForm: DirektuploadForm; uploading: boolean
|
||||
onTitleChange: (v: string) => void; onBack: () => void; onUpload: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-800 mb-2">Zusammenfassung</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">Pruefen Sie Ihre Eingaben und starten Sie die Korrektur.</p>
|
||||
<div className="bg-slate-50 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Titel</span>
|
||||
<input
|
||||
type="text" value={direktForm.klausurTitle}
|
||||
onChange={(e) => onTitleChange(e.target.value)}
|
||||
className="text-sm font-medium text-slate-800 bg-white border border-slate-200 rounded px-2 py-1 text-right"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Anzahl Arbeiten</span>
|
||||
<span className="text-sm font-medium text-slate-800">{direktForm.files.length}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Aufgabentyp</span>
|
||||
<span className="text-sm font-medium text-slate-800">{direktForm.aufgabentyp || 'Nicht angegeben'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Erwartungshorizont</span>
|
||||
<span className="text-sm font-medium text-slate-800">{direktForm.ehText ? 'Vorhanden' : 'Nicht angegeben'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-blue-600 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div className="text-sm text-blue-800">
|
||||
<p className="font-medium">Was passiert jetzt?</p>
|
||||
<ol className="list-decimal list-inside mt-1 space-y-1 text-blue-700">
|
||||
<li>Eine neue Klausur wird automatisch erstellt</li>
|
||||
<li>Alle {direktForm.files.length} Arbeiten werden hochgeladen</li>
|
||||
<li>OCR-Erkennung der Handschrift startet automatisch</li>
|
||||
<li>Sie werden zur Korrektur-Ansicht weitergeleitet</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between">
|
||||
<button onClick={onBack} className="px-6 py-2 text-slate-600 hover:bg-slate-100 rounded-lg">Zurueck</button>
|
||||
<button
|
||||
onClick={onUpload}
|
||||
disabled={uploading}
|
||||
className="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
{uploading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Wird hochgeladen...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
Korrektur starten
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user