fix: Redesign StepUpload for manual step control
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 <noreply@anthropic.com>
This commit is contained in:
@@ -53,11 +53,12 @@ function OcrKombiContent() {
|
||||
case 0:
|
||||
return (
|
||||
<StepUpload
|
||||
sessionId={sessionId}
|
||||
onUploaded={(sid) => {
|
||||
setSessionId(sid)
|
||||
loadSessions()
|
||||
openSession(sid)
|
||||
}}
|
||||
onNext={handleNext}
|
||||
/>
|
||||
)
|
||||
case 1:
|
||||
|
||||
@@ -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<File | null>(null)
|
||||
const [preview, setPreview] = useState<string | null>(null)
|
||||
const [title, setTitle] = useState('')
|
||||
const [category, setCategory] = useState<DocumentCategory>('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<HTMLInputElement>) => {
|
||||
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 text-green-700 dark:text-green-300 text-sm font-medium mb-3">
|
||||
<span>✓</span> Dokument hochgeladen
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="w-48 h-64 rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-700 flex-shrink-0 border border-gray-200 dark:border-gray-600">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image`}
|
||||
alt="Hochgeladenes Dokument"
|
||||
className="w-full h-full object-contain"
|
||||
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
{title || 'Dokument'}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
Kategorie: {DOCUMENT_CATEGORIES.find(c => c.value === category)?.label || category}
|
||||
</div>
|
||||
<div className="text-xs font-mono text-gray-400 mt-1">
|
||||
Session: {sessionId.slice(0, 8)}...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={onNext}
|
||||
className="px-6 py-2.5 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 transition-colors"
|
||||
>
|
||||
Weiter →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- Phase 1b: File selected → preview + "Hochladen" ----
|
||||
if (selectedFile) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Title input */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Titel
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Category selector */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Kategorie
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-1.5">
|
||||
{DOCUMENT_CATEGORIES.map(cat => (
|
||||
<button
|
||||
key={cat.value}
|
||||
onClick={() => setCategory(cat.value)}
|
||||
className={`text-xs px-2 py-1.5 rounded-md text-left transition-colors ${
|
||||
category === cat.value
|
||||
? 'bg-teal-100 dark:bg-teal-900/40 text-teal-700 dark:text-teal-300 ring-1 ring-teal-400'
|
||||
: 'bg-gray-50 dark:bg-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{cat.icon} {cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File preview */}
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-xl p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{preview ? (
|
||||
<div className="w-36 h-48 rounded-lg overflow-hidden bg-gray-100 dark:bg-gray-700 flex-shrink-0 border border-gray-200 dark:border-gray-600">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={preview} alt="Vorschau" className="w-full h-full object-contain" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-36 h-48 rounded-lg bg-gray-100 dark:bg-gray-700 flex-shrink-0 flex items-center justify-center border border-gray-200 dark:border-gray-600">
|
||||
<span className="text-3xl">📄</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-gray-700 dark:text-gray-300 truncate">
|
||||
{selectedFile.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{(selectedFile.size / 1024 / 1024).toFixed(1)} MB
|
||||
</div>
|
||||
<button
|
||||
onClick={clearFile}
|
||||
className="text-xs text-red-500 hover:text-red-700 mt-2"
|
||||
>
|
||||
Andere Datei waehlen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
disabled={uploading}
|
||||
className="mt-4 w-full px-4 py-2.5 bg-teal-600 text-white text-sm font-medium rounded-lg hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{uploading ? 'Wird hochgeladen...' : 'Hochladen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 bg-red-50 dark:bg-red-900/20 p-3 rounded-lg">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ---- Phase 1a: No file → drop zone ----
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Title input */}
|
||||
@@ -116,25 +278,19 @@ export function StepUpload({ onUploaded }: StepUploadProps) {
|
||||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
|
||||
}`}
|
||||
>
|
||||
{uploading ? (
|
||||
<div className="text-sm text-gray-500">Wird hochgeladen...</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-4xl mb-3">📤</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
Bild oder PDF hierher ziehen
|
||||
</div>
|
||||
<label className="inline-block px-4 py-2 bg-teal-600 text-white text-sm rounded-lg cursor-pointer hover:bg-teal-700">
|
||||
Datei auswaehlen
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.pdf"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<div className="text-4xl mb-3">📤</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
Bild oder PDF hierher ziehen
|
||||
</div>
|
||||
<label className="inline-block px-4 py-2 bg-teal-600 text-white text-sm rounded-lg cursor-pointer hover:bg-teal-700">
|
||||
Datei auswaehlen
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*,.pdf"
|
||||
onChange={handleInputChange}
|
||||
className="hidden"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
|
||||
Reference in New Issue
Block a user