From 469f09d1e1d3f56acd6d7bde9024137d28bedd3a Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Mar 2026 17:35:36 +0100 Subject: [PATCH] fix: Redesign StepUpload for manual step control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit StepUpload now has 3 phases: 1. File selection: drop zone / file picker → shows preview 2. Review: title input, category, file info → "Hochladen" button 3. Uploaded: shows session image → "Weiter" button No more auto-advance after upload. User controls every step. openSession() removed from onUploaded callback to prevent step-reset race condition. Co-Authored-By: Claude Opus 4.6 --- .../app/(admin)/ai/ocr-kombi/page.tsx | 3 +- .../components/ocr-kombi/StepUpload.tsx | 214 +++++++++++++++--- 2 files changed, 187 insertions(+), 30 deletions(-) diff --git a/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx b/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx index cab6f0f..9eafaee 100644 --- a/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx +++ b/admin-lehrer/app/(admin)/ai/ocr-kombi/page.tsx @@ -53,11 +53,12 @@ function OcrKombiContent() { case 0: return ( { setSessionId(sid) loadSessions() - openSession(sid) }} + onNext={handleNext} /> ) case 1: diff --git a/admin-lehrer/components/ocr-kombi/StepUpload.tsx b/admin-lehrer/components/ocr-kombi/StepUpload.tsx index def1e85..5c21aa4 100644 --- a/admin-lehrer/components/ocr-kombi/StepUpload.tsx +++ b/admin-lehrer/components/ocr-kombi/StepUpload.tsx @@ -1,28 +1,52 @@ 'use client' -import { useState, useCallback } from 'react' +import { useState, useCallback, useEffect } from 'react' import { DOCUMENT_CATEGORIES, type DocumentCategory } from '@/app/(admin)/ai/ocr-pipeline/types' const KLAUSUR_API = '/klausur-api' interface StepUploadProps { + sessionId: string | null onUploaded: (sessionId: string) => void + onNext: () => void } -export function StepUpload({ onUploaded }: StepUploadProps) { +export function StepUpload({ sessionId, onUploaded, onNext }: StepUploadProps) { const [dragging, setDragging] = useState(false) const [uploading, setUploading] = useState(false) + const [selectedFile, setSelectedFile] = useState(null) + const [preview, setPreview] = useState(null) const [title, setTitle] = useState('') const [category, setCategory] = useState('vokabelseite') const [error, setError] = useState('') - const handleUpload = useCallback(async (file: File) => { + // Clean up preview URL on unmount + useEffect(() => { + return () => { if (preview) URL.revokeObjectURL(preview) } + }, [preview]) + + const handleFileSelect = useCallback((file: File) => { + setSelectedFile(file) + setError('') + if (file.type.startsWith('image/')) { + setPreview(URL.createObjectURL(file)) + } else { + setPreview(null) + } + // Auto-fill title from filename if empty + if (!title.trim()) { + setTitle(file.name.replace(/\.[^.]+$/, '')) + } + }, [title]) + + const handleUpload = useCallback(async () => { + if (!selectedFile) return setUploading(true) setError('') try { const formData = new FormData() - formData.append('file', file) + formData.append('file', selectedFile) if (title.trim()) formData.append('name', title.trim()) const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`, { @@ -53,20 +77,158 @@ export function StepUpload({ onUploaded }: StepUploadProps) { } finally { setUploading(false) } - }, [title, category, onUploaded]) + }, [selectedFile, title, category, onUploaded]) const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() setDragging(false) const file = e.dataTransfer.files[0] - if (file) handleUpload(file) - }, [handleUpload]) + if (file) handleFileSelect(file) + }, [handleFileSelect]) - const handleFileSelect = useCallback((e: React.ChangeEvent) => { + const handleInputChange = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0] - if (file) handleUpload(file) - }, [handleUpload]) + if (file) handleFileSelect(file) + }, [handleFileSelect]) + const clearFile = useCallback(() => { + setSelectedFile(null) + if (preview) URL.revokeObjectURL(preview) + setPreview(null) + }, [preview]) + + // ---- Phase 2: Uploaded → show result + "Weiter" ---- + if (sessionId) { + return ( +
+
+
+ Dokument hochgeladen +
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Hochgeladenes Dokument { (e.target as HTMLImageElement).style.display = 'none' }} + /> +
+
+
+ {title || 'Dokument'} +
+
+ Kategorie: {DOCUMENT_CATEGORIES.find(c => c.value === category)?.label || category} +
+
+ Session: {sessionId.slice(0, 8)}... +
+
+
+
+ +
+ +
+
+ ) + } + + // ---- Phase 1b: File selected → preview + "Hochladen" ---- + if (selectedFile) { + return ( +
+ {/* Title input */} +
+ + setTitle(e.target.value)} + placeholder="z.B. Vokabeln Unit 3" + className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-sm" + /> +
+ + {/* Category selector */} +
+ +
+ {DOCUMENT_CATEGORIES.map(cat => ( + + ))} +
+
+ + {/* File preview */} +
+
+ {preview ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + Vorschau +
+ ) : ( +
+ 📄 +
+ )} +
+
+ {selectedFile.name} +
+
+ {(selectedFile.size / 1024 / 1024).toFixed(1)} MB +
+ +
+
+ + +
+ + {error && ( +
+ {error} +
+ )} +
+ ) + } + + // ---- Phase 1a: No file → drop zone ---- return (
{/* Title input */} @@ -116,25 +278,19 @@ export function StepUpload({ onUploaded }: StepUploadProps) { : 'border-gray-300 dark:border-gray-600 hover:border-gray-400' }`} > - {uploading ? ( -
Wird hochgeladen...
- ) : ( - <> -
📤
-
- Bild oder PDF hierher ziehen -
- - - )} +
📤
+
+ Bild oder PDF hierher ziehen +
+
{error && (