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:
|
case 0:
|
||||||
return (
|
return (
|
||||||
<StepUpload
|
<StepUpload
|
||||||
|
sessionId={sessionId}
|
||||||
onUploaded={(sid) => {
|
onUploaded={(sid) => {
|
||||||
setSessionId(sid)
|
setSessionId(sid)
|
||||||
loadSessions()
|
loadSessions()
|
||||||
openSession(sid)
|
|
||||||
}}
|
}}
|
||||||
|
onNext={handleNext}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
case 1:
|
case 1:
|
||||||
|
|||||||
@@ -1,28 +1,52 @@
|
|||||||
'use client'
|
'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'
|
import { DOCUMENT_CATEGORIES, type DocumentCategory } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||||
|
|
||||||
const KLAUSUR_API = '/klausur-api'
|
const KLAUSUR_API = '/klausur-api'
|
||||||
|
|
||||||
interface StepUploadProps {
|
interface StepUploadProps {
|
||||||
|
sessionId: string | null
|
||||||
onUploaded: (sessionId: string) => void
|
onUploaded: (sessionId: string) => void
|
||||||
|
onNext: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StepUpload({ onUploaded }: StepUploadProps) {
|
export function StepUpload({ sessionId, onUploaded, onNext }: StepUploadProps) {
|
||||||
const [dragging, setDragging] = useState(false)
|
const [dragging, setDragging] = useState(false)
|
||||||
const [uploading, setUploading] = 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 [title, setTitle] = useState('')
|
||||||
const [category, setCategory] = useState<DocumentCategory>('vokabelseite')
|
const [category, setCategory] = useState<DocumentCategory>('vokabelseite')
|
||||||
const [error, setError] = useState('')
|
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)
|
setUploading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', selectedFile)
|
||||||
if (title.trim()) formData.append('name', title.trim())
|
if (title.trim()) formData.append('name', title.trim())
|
||||||
|
|
||||||
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`, {
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions`, {
|
||||||
@@ -53,20 +77,158 @@ export function StepUpload({ onUploaded }: StepUploadProps) {
|
|||||||
} finally {
|
} finally {
|
||||||
setUploading(false)
|
setUploading(false)
|
||||||
}
|
}
|
||||||
}, [title, category, onUploaded])
|
}, [selectedFile, title, category, onUploaded])
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setDragging(false)
|
setDragging(false)
|
||||||
const file = e.dataTransfer.files[0]
|
const file = e.dataTransfer.files[0]
|
||||||
if (file) handleUpload(file)
|
if (file) handleFileSelect(file)
|
||||||
}, [handleUpload])
|
}, [handleFileSelect])
|
||||||
|
|
||||||
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0]
|
||||||
if (file) handleUpload(file)
|
if (file) handleFileSelect(file)
|
||||||
}, [handleUpload])
|
}, [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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Title input */}
|
{/* Title input */}
|
||||||
@@ -116,25 +278,19 @@ export function StepUpload({ onUploaded }: StepUploadProps) {
|
|||||||
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
|
: 'border-gray-300 dark:border-gray-600 hover:border-gray-400'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{uploading ? (
|
<div className="text-4xl mb-3">📤</div>
|
||||||
<div className="text-sm text-gray-500">Wird hochgeladen...</div>
|
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||||
) : (
|
Bild oder PDF hierher ziehen
|
||||||
<>
|
</div>
|
||||||
<div className="text-4xl mb-3">📤</div>
|
<label className="inline-block px-4 py-2 bg-teal-600 text-white text-sm rounded-lg cursor-pointer hover:bg-teal-700">
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
Datei auswaehlen
|
||||||
Bild oder PDF hierher ziehen
|
<input
|
||||||
</div>
|
type="file"
|
||||||
<label className="inline-block px-4 py-2 bg-teal-600 text-white text-sm rounded-lg cursor-pointer hover:bg-teal-700">
|
accept="image/*,.pdf"
|
||||||
Datei auswaehlen
|
onChange={handleInputChange}
|
||||||
<input
|
className="hidden"
|
||||||
type="file"
|
/>
|
||||||
accept="image/*,.pdf"
|
</label>
|
||||||
onChange={handleFileSelect}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
Reference in New Issue
Block a user