feat: Slide-Modus als alternative Wort-Positionierung im Overlay
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 34s
CI / test-go-edu-search (push) Successful in 33s
CI / test-python-klausur (push) Failing after 2m9s
CI / test-python-agent-core (push) Successful in 23s
CI / test-nodejs-website (push) Successful in 24s

Neuer Hook useSlideWordPositions: Schiebt alle erkannten Woerter von links
nach rechts ueber die Pixel-Projektion bis jedes Wort auf seiner Tinte
einrastet. Kein Wort geht verloren, keine Cluster-Matching-Regeln noetig.

Toggle-Button (Slide/Cluster) in der Overlay-Toolbar zum Umschalten.
Bestehender Cluster-Algorithmus bleibt als Alternative erhalten.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-11 16:13:31 +01:00
parent 2f51ac617f
commit bc13978bc1
2 changed files with 252 additions and 2 deletions

View File

@@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { GridResult, GridCell, RowResult, RowItem } from '@/app/(admin)/ai/ocr-overlay/types'
import { usePixelWordPositions } from './usePixelWordPositions'
import { useSlideWordPositions } from './useSlideWordPositions'
const KLAUSUR_API = '/klausur-api'
@@ -42,19 +43,27 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
const [imageRotation, setImageRotation] = useState<0 | 180>(0)
const [textOpacity, setTextOpacity] = useState(100)
const [textColor, setTextColor] = useState<'red' | 'blue' | 'black'>('red')
const [positioningMode, setPositioningMode] = useState<'cluster' | 'slide'>('slide')
const reconRef = useRef<HTMLDivElement>(null)
const [reconWidth, setReconWidth] = useState(0)
// Pixel-based word positions
// Pixel-based word positions (both algorithms run, toggle selects which to use)
const overlayImageUrl = sessionId
? `${KLAUSUR_API}/api/v1/ocr-pipeline/sessions/${sessionId}/image/cropped`
: ''
const cellWordPositions = usePixelWordPositions(
const clusterPositions = usePixelWordPositions(
overlayImageUrl,
gridCells,
status === 'ready',
imageRotation,
)
const slidePositions = useSlideWordPositions(
overlayImageUrl,
gridCells,
status === 'ready',
imageRotation,
)
const cellWordPositions = positioningMode === 'slide' ? slidePositions : clusterPositions
// Track container width
useEffect(() => {
@@ -395,6 +404,23 @@ export function OverlayReconstruction({ sessionId, onNext }: OverlayReconstructi
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Positioning mode toggle */}
<button
onClick={() => setPositioningMode(m => m === 'slide' ? 'cluster' : 'slide')}
className={`px-2 py-1 text-xs rounded border transition-colors ${
positioningMode === 'slide'
? 'bg-orange-500 text-white border-orange-500'
: 'bg-white dark:bg-gray-700 text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600'
}`}
title={positioningMode === 'slide'
? 'Slide-Modus: Woerter von links nach rechts schieben (klick fuer Cluster-Modus)'
: 'Cluster-Modus: Woerter an Pixel-Cluster zuordnen (klick fuer Slide-Modus)'}
>
{positioningMode === 'slide' ? 'Slide' : 'Cluster'}
</button>
<div className="w-px h-5 bg-gray-300 dark:bg-gray-600 mx-1" />
{/* Text color */}
{(['red', 'blue', 'black'] as const).map(c => (
<button