feat: Frontend Sub-Sessions (Boxen) in OCR-Pipeline UI
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 29s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 1m57s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 18s
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 29s
CI / test-go-edu-search (push) Successful in 29s
CI / test-python-klausur (push) Failing after 1m57s
CI / test-python-agent-core (push) Successful in 17s
CI / test-nodejs-website (push) Successful in 18s
- BoxSessionTabs: Tab-Leiste zum Wechsel zwischen Haupt- und Box-Sessions - StepColumnDetection: Box-Info + "Box-Sessions erstellen" Button - page.tsx: Session-Wechsel, Sub-Session-State, auto-return nach Abschluss - types.ts: SubSession, PageZone, erweiterte SessionInfo/ColumnResult Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,8 @@ import { StepWordRecognition } from '@/components/ocr-pipeline/StepWordRecogniti
|
|||||||
import { StepLlmReview } from '@/components/ocr-pipeline/StepLlmReview'
|
import { StepLlmReview } from '@/components/ocr-pipeline/StepLlmReview'
|
||||||
import { StepReconstruction } from '@/components/ocr-pipeline/StepReconstruction'
|
import { StepReconstruction } from '@/components/ocr-pipeline/StepReconstruction'
|
||||||
import { StepGroundTruth } from '@/components/ocr-pipeline/StepGroundTruth'
|
import { StepGroundTruth } from '@/components/ocr-pipeline/StepGroundTruth'
|
||||||
import { PIPELINE_STEPS, DOCUMENT_CATEGORIES, type PipelineStep, type SessionListItem, type DocumentTypeResult, type DocumentCategory } from './types'
|
import { BoxSessionTabs } from '@/components/ocr-pipeline/BoxSessionTabs'
|
||||||
|
import { PIPELINE_STEPS, DOCUMENT_CATEGORIES, type PipelineStep, type SessionListItem, type DocumentTypeResult, type DocumentCategory, type SubSession } from './types'
|
||||||
|
|
||||||
const KLAUSUR_API = '/klausur-api'
|
const KLAUSUR_API = '/klausur-api'
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ export default function OcrPipelinePage() {
|
|||||||
const [editingCategory, setEditingCategory] = useState<string | null>(null)
|
const [editingCategory, setEditingCategory] = useState<string | null>(null)
|
||||||
const [docTypeResult, setDocTypeResult] = useState<DocumentTypeResult | null>(null)
|
const [docTypeResult, setDocTypeResult] = useState<DocumentTypeResult | null>(null)
|
||||||
const [activeCategory, setActiveCategory] = useState<DocumentCategory | undefined>(undefined)
|
const [activeCategory, setActiveCategory] = useState<DocumentCategory | undefined>(undefined)
|
||||||
|
const [subSessions, setSubSessions] = useState<SubSession[]>([])
|
||||||
|
const [parentSessionId, setParentSessionId] = useState<string | null>(null)
|
||||||
const [steps, setSteps] = useState<PipelineStep[]>(
|
const [steps, setSteps] = useState<PipelineStep[]>(
|
||||||
PIPELINE_STEPS.map((s, i) => ({
|
PIPELINE_STEPS.map((s, i) => ({
|
||||||
...s,
|
...s,
|
||||||
@@ -55,7 +58,7 @@ export default function OcrPipelinePage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openSession = useCallback(async (sid: string) => {
|
const openSession = useCallback(async (sid: string, keepSubSessions?: boolean) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`)
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sid}`)
|
||||||
if (!res.ok) return
|
if (!res.ok) return
|
||||||
@@ -65,6 +68,18 @@ export default function OcrPipelinePage() {
|
|||||||
setSessionName(data.name || data.filename || '')
|
setSessionName(data.name || data.filename || '')
|
||||||
setActiveCategory(data.document_category || undefined)
|
setActiveCategory(data.document_category || undefined)
|
||||||
|
|
||||||
|
// Sub-session handling
|
||||||
|
if (data.sub_sessions && data.sub_sessions.length > 0) {
|
||||||
|
setSubSessions(data.sub_sessions)
|
||||||
|
setParentSessionId(sid)
|
||||||
|
} else if (data.parent_session_id) {
|
||||||
|
// This is a sub-session — keep parent info but don't reset sub-session list
|
||||||
|
setParentSessionId(data.parent_session_id)
|
||||||
|
} else if (!keepSubSessions) {
|
||||||
|
setSubSessions([])
|
||||||
|
setParentSessionId(null)
|
||||||
|
}
|
||||||
|
|
||||||
// Restore doc type result if available
|
// Restore doc type result if available
|
||||||
const savedDocType: DocumentTypeResult | null = data.doc_type_result || null
|
const savedDocType: DocumentTypeResult | null = data.doc_type_result || null
|
||||||
setDocTypeResult(savedDocType)
|
setDocTypeResult(savedDocType)
|
||||||
@@ -98,6 +113,8 @@ export default function OcrPipelinePage() {
|
|||||||
setSessionId(null)
|
setSessionId(null)
|
||||||
setCurrentStep(0)
|
setCurrentStep(0)
|
||||||
setDocTypeResult(null)
|
setDocTypeResult(null)
|
||||||
|
setSubSessions([])
|
||||||
|
setParentSessionId(null)
|
||||||
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -144,6 +161,8 @@ export default function OcrPipelinePage() {
|
|||||||
setCurrentStep(0)
|
setCurrentStep(0)
|
||||||
setDocTypeResult(null)
|
setDocTypeResult(null)
|
||||||
setActiveCategory(undefined)
|
setActiveCategory(undefined)
|
||||||
|
setSubSessions([])
|
||||||
|
setParentSessionId(null)
|
||||||
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to delete all sessions:', e)
|
console.error('Failed to delete all sessions:', e)
|
||||||
@@ -168,10 +187,22 @@ export default function OcrPipelinePage() {
|
|||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (currentStep >= steps.length - 1) {
|
if (currentStep >= steps.length - 1) {
|
||||||
// Last step completed — return to session list
|
// Last step completed
|
||||||
|
if (parentSessionId && sessionId !== parentSessionId) {
|
||||||
|
// Sub-session completed — update its status and stay in tab view
|
||||||
|
setSubSessions((prev) =>
|
||||||
|
prev.map((s) => s.id === sessionId ? { ...s, status: 'completed', current_step: 10 } : s)
|
||||||
|
)
|
||||||
|
// Switch back to parent
|
||||||
|
handleSessionChange(parentSessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Main session: return to session list
|
||||||
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||||
setCurrentStep(0)
|
setCurrentStep(0)
|
||||||
setSessionId(null)
|
setSessionId(null)
|
||||||
|
setSubSessions([])
|
||||||
|
setParentSessionId(null)
|
||||||
loadSessions()
|
loadSessions()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -268,9 +299,20 @@ export default function OcrPipelinePage() {
|
|||||||
setSessionName('')
|
setSessionName('')
|
||||||
setCurrentStep(0)
|
setCurrentStep(0)
|
||||||
setDocTypeResult(null)
|
setDocTypeResult(null)
|
||||||
|
setSubSessions([])
|
||||||
|
setParentSessionId(null)
|
||||||
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
setSteps(PIPELINE_STEPS.map((s, i) => ({ ...s, status: i === 0 ? 'active' : 'pending' })))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSessionChange = useCallback((newSessionId: string) => {
|
||||||
|
openSession(newSessionId, true)
|
||||||
|
}, [openSession])
|
||||||
|
|
||||||
|
const handleBoxSessionsCreated = useCallback((subs: SubSession[]) => {
|
||||||
|
setSubSessions(subs)
|
||||||
|
if (sessionId) setParentSessionId(sessionId)
|
||||||
|
}, [sessionId])
|
||||||
|
|
||||||
const stepNames: Record<number, string> = {
|
const stepNames: Record<number, string> = {
|
||||||
1: 'Orientierung',
|
1: 'Orientierung',
|
||||||
2: 'Begradigung',
|
2: 'Begradigung',
|
||||||
@@ -318,7 +360,7 @@ export default function OcrPipelinePage() {
|
|||||||
case 3:
|
case 3:
|
||||||
return <StepCrop sessionId={sessionId} onNext={handleCropNext} />
|
return <StepCrop sessionId={sessionId} onNext={handleCropNext} />
|
||||||
case 4:
|
case 4:
|
||||||
return <StepColumnDetection sessionId={sessionId} onNext={handleNext} />
|
return <StepColumnDetection sessionId={sessionId} onNext={handleNext} onBoxSessionsCreated={handleBoxSessionsCreated} />
|
||||||
case 5:
|
case 5:
|
||||||
return <StepRowDetection sessionId={sessionId} onNext={handleNext} />
|
return <StepRowDetection sessionId={sessionId} onNext={handleNext} />
|
||||||
case 6:
|
case 6:
|
||||||
@@ -552,6 +594,15 @@ export default function OcrPipelinePage() {
|
|||||||
onDocTypeChange={handleDocTypeChange}
|
onDocTypeChange={handleDocTypeChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{subSessions.length > 0 && parentSessionId && sessionId && (
|
||||||
|
<BoxSessionTabs
|
||||||
|
parentSessionId={parentSessionId}
|
||||||
|
subSessions={subSessions}
|
||||||
|
activeSessionId={sessionId}
|
||||||
|
onSessionChange={handleSessionChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="min-h-[400px]">{renderStep()}</div>
|
<div className="min-h-[400px]">{renderStep()}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ export interface SessionListItem {
|
|||||||
doc_type?: string
|
doc_type?: string
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
|
parent_session_id?: string | null
|
||||||
|
box_index?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubSession {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
box_index: number
|
||||||
|
current_step?: number
|
||||||
|
status?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PipelineLogEntry {
|
export interface PipelineLogEntry {
|
||||||
@@ -95,6 +105,9 @@ export interface SessionInfo {
|
|||||||
row_result?: RowResult
|
row_result?: RowResult
|
||||||
word_result?: GridResult
|
word_result?: GridResult
|
||||||
doc_type_result?: DocumentTypeResult
|
doc_type_result?: DocumentTypeResult
|
||||||
|
sub_sessions?: SubSession[]
|
||||||
|
parent_session_id?: string
|
||||||
|
box_index?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeskewResult {
|
export interface DeskewResult {
|
||||||
@@ -151,9 +164,17 @@ export interface PageRegion {
|
|||||||
classification_method?: string
|
classification_method?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageZone {
|
||||||
|
zone_type: 'content' | 'box'
|
||||||
|
y_start: number
|
||||||
|
y_end: number
|
||||||
|
box?: { x: number; y: number; width: number; height: number }
|
||||||
|
}
|
||||||
|
|
||||||
export interface ColumnResult {
|
export interface ColumnResult {
|
||||||
columns: PageRegion[]
|
columns: PageRegion[]
|
||||||
duration_seconds: number
|
duration_seconds: number
|
||||||
|
zones?: PageZone[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnGroundTruth {
|
export interface ColumnGroundTruth {
|
||||||
|
|||||||
68
admin-lehrer/components/ocr-pipeline/BoxSessionTabs.tsx
Normal file
68
admin-lehrer/components/ocr-pipeline/BoxSessionTabs.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { SubSession } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||||
|
|
||||||
|
interface BoxSessionTabsProps {
|
||||||
|
parentSessionId: string
|
||||||
|
subSessions: SubSession[]
|
||||||
|
activeSessionId: string
|
||||||
|
onSessionChange: (sessionId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_ICONS: Record<string, string> = {
|
||||||
|
pending: '\u23F3', // hourglass
|
||||||
|
processing: '\uD83D\uDD04', // arrows
|
||||||
|
completed: '\u2713', // checkmark
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusIcon(sub: SubSession): string {
|
||||||
|
if (sub.status === 'completed' || (sub.current_step && sub.current_step >= 9)) return STATUS_ICONS.completed
|
||||||
|
if (sub.current_step && sub.current_step > 1) return STATUS_ICONS.processing
|
||||||
|
return STATUS_ICONS.pending
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BoxSessionTabs({ parentSessionId, subSessions, activeSessionId, onSessionChange }: BoxSessionTabsProps) {
|
||||||
|
if (subSessions.length === 0) return null
|
||||||
|
|
||||||
|
const isParentActive = activeSessionId === parentSessionId
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1.5 px-1 py-1.5 bg-gray-50 dark:bg-gray-800/50 rounded-xl border border-gray-200 dark:border-gray-700">
|
||||||
|
{/* Main session tab */}
|
||||||
|
<button
|
||||||
|
onClick={() => onSessionChange(parentSessionId)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||||
|
isParentActive
|
||||||
|
? 'bg-white dark:bg-gray-700 text-teal-700 dark:text-teal-400 shadow-sm ring-1 ring-teal-300 dark:ring-teal-600'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-700/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Hauptseite
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="w-px h-5 bg-gray-200 dark:bg-gray-700" />
|
||||||
|
|
||||||
|
{/* Sub-session tabs */}
|
||||||
|
{subSessions.map((sub) => {
|
||||||
|
const isActive = activeSessionId === sub.id
|
||||||
|
const icon = getStatusIcon(sub)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={sub.id}
|
||||||
|
onClick={() => onSessionChange(sub.id)}
|
||||||
|
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${
|
||||||
|
isActive
|
||||||
|
? 'bg-white dark:bg-gray-700 text-teal-700 dark:text-teal-400 shadow-sm ring-1 ring-teal-300 dark:ring-teal-600'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-700/50'
|
||||||
|
}`}
|
||||||
|
title={sub.name}
|
||||||
|
>
|
||||||
|
<span className="mr-1">{icon}</span>
|
||||||
|
Box {sub.box_index + 1}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import type { ColumnResult, ColumnGroundTruth, PageRegion } from '@/app/(admin)/ai/ocr-pipeline/types'
|
import type { ColumnResult, ColumnGroundTruth, PageRegion, SubSession } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||||
import { ColumnControls } from './ColumnControls'
|
import { ColumnControls } from './ColumnControls'
|
||||||
import { ManualColumnEditor } from './ManualColumnEditor'
|
import { ManualColumnEditor } from './ManualColumnEditor'
|
||||||
import type { ColumnTypeKey } from '@/app/(admin)/ai/ocr-pipeline/types'
|
import type { ColumnTypeKey } from '@/app/(admin)/ai/ocr-pipeline/types'
|
||||||
@@ -13,6 +13,7 @@ type ViewMode = 'normal' | 'ground-truth' | 'manual'
|
|||||||
interface StepColumnDetectionProps {
|
interface StepColumnDetectionProps {
|
||||||
sessionId: string | null
|
sessionId: string | null
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
|
onBoxSessionsCreated?: (subSessions: SubSession[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Convert PageRegion[] to divider percentages + column types for ManualColumnEditor */
|
/** Convert PageRegion[] to divider percentages + column types for ManualColumnEditor */
|
||||||
@@ -34,7 +35,7 @@ function columnsToEditorState(
|
|||||||
return { dividers, columnTypes }
|
return { dividers, columnTypes }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionProps) {
|
export function StepColumnDetection({ sessionId, onNext, onBoxSessionsCreated }: StepColumnDetectionProps) {
|
||||||
const [columnResult, setColumnResult] = useState<ColumnResult | null>(null)
|
const [columnResult, setColumnResult] = useState<ColumnResult | null>(null)
|
||||||
const [detecting, setDetecting] = useState(false)
|
const [detecting, setDetecting] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
@@ -42,6 +43,8 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
|||||||
const [applying, setApplying] = useState(false)
|
const [applying, setApplying] = useState(false)
|
||||||
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null)
|
const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null)
|
||||||
const [savedGtColumns, setSavedGtColumns] = useState<PageRegion[] | null>(null)
|
const [savedGtColumns, setSavedGtColumns] = useState<PageRegion[] | null>(null)
|
||||||
|
const [creatingBoxSessions, setCreatingBoxSessions] = useState(false)
|
||||||
|
const [existingSubSessions, setExistingSubSessions] = useState<SubSession[] | null>(null)
|
||||||
|
|
||||||
// Fetch session info (image dimensions) + check for cached column result
|
// Fetch session info (image dimensions) + check for cached column result
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,6 +58,10 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
|||||||
if (info.image_width && info.image_height) {
|
if (info.image_width && info.image_height) {
|
||||||
setImageDimensions({ width: info.image_width, height: info.image_height })
|
setImageDimensions({ width: info.image_width, height: info.image_height })
|
||||||
}
|
}
|
||||||
|
if (info.sub_sessions && info.sub_sessions.length > 0) {
|
||||||
|
setExistingSubSessions(info.sub_sessions)
|
||||||
|
onBoxSessionsCreated?.(info.sub_sessions)
|
||||||
|
}
|
||||||
if (info.column_result) {
|
if (info.column_result) {
|
||||||
setColumnResult(info.column_result)
|
setColumnResult(info.column_result)
|
||||||
return
|
return
|
||||||
@@ -178,6 +185,39 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
|||||||
}
|
}
|
||||||
}, [sessionId])
|
}, [sessionId])
|
||||||
|
|
||||||
|
// Count box zones from column result
|
||||||
|
const boxZones = columnResult?.zones?.filter(z => z.zone_type === 'box') || []
|
||||||
|
const boxCount = boxZones.length
|
||||||
|
|
||||||
|
const createBoxSessions = useCallback(async () => {
|
||||||
|
if (!sessionId) return
|
||||||
|
setCreatingBoxSessions(true)
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/create-box-sessions`, {
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({ detail: res.statusText }))
|
||||||
|
throw new Error(err.detail || 'Box-Sessions konnten nicht erstellt werden')
|
||||||
|
}
|
||||||
|
const data = await res.json()
|
||||||
|
const subs: SubSession[] = data.sub_sessions.map((s: { id: string; name?: string; box_index: number }) => ({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name || `Box ${s.box_index + 1}`,
|
||||||
|
box_index: s.box_index,
|
||||||
|
current_step: 1,
|
||||||
|
status: 'pending',
|
||||||
|
}))
|
||||||
|
setExistingSubSessions(subs)
|
||||||
|
onBoxSessionsCreated?.(subs)
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : 'Fehler beim Erstellen der Box-Sessions')
|
||||||
|
} finally {
|
||||||
|
setCreatingBoxSessions(false)
|
||||||
|
}
|
||||||
|
}, [sessionId, onBoxSessionsCreated])
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-16 text-center">
|
<div className="flex flex-col items-center justify-center py-16 text-center">
|
||||||
@@ -317,6 +357,39 @@ export function StepColumnDetection({ sessionId, onNext }: StepColumnDetectionPr
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Box zone info */}
|
||||||
|
{viewMode === 'normal' && boxCount > 0 && (
|
||||||
|
<div className="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700 rounded-xl p-4 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl">📦</span>
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-amber-800 dark:text-amber-300">
|
||||||
|
{boxCount} Box{boxCount > 1 ? 'en' : ''} erkannt
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
Box-Bereiche werden separat verarbeitet
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{existingSubSessions && existingSubSessions.length > 0 ? (
|
||||||
|
<div className="text-xs text-amber-700 dark:text-amber-300 font-medium">
|
||||||
|
{existingSubSessions.length} Box-Session{existingSubSessions.length > 1 ? 's' : ''} vorhanden
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={createBoxSessions}
|
||||||
|
disabled={creatingBoxSessions}
|
||||||
|
className="px-4 py-2 bg-amber-600 text-white rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium disabled:opacity-50 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{creatingBoxSessions && (
|
||||||
|
<div className="animate-spin w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full" />
|
||||||
|
)}
|
||||||
|
Box-Sessions erstellen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
{viewMode === 'normal' && (
|
{viewMode === 'normal' && (
|
||||||
<ColumnControls
|
<ColumnControls
|
||||||
|
|||||||
Reference in New Issue
Block a user