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 42s
CI / test-go-edu-search (push) Successful in 34s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 21s
CI / test-nodejs-website (push) Successful in 29s
sed replacement left orphaned hostname references in story page and empty lines in getApiBase functions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
305 lines
11 KiB
TypeScript
305 lines
11 KiB
TypeScript
'use client'
|
|
|
|
import { SkeletonOCRResult, SkeletonDots } from '@/components/common/SkeletonText'
|
|
import { ConfidenceHeatmap } from '@/components/ai/ConfidenceHeatmap'
|
|
import type { OCRResult, MagicSettings } from '../types'
|
|
|
|
interface TabTestProps {
|
|
ocrResult: OCRResult | null
|
|
ocrLoading: boolean
|
|
imagePreview: string | null
|
|
uploadedImage: File | null
|
|
settings: MagicSettings
|
|
showHeatmap: boolean
|
|
onToggleHeatmap: () => void
|
|
onFileUpload: (file: File) => void
|
|
onManualOCR: () => void
|
|
onClearImage: () => void
|
|
onSendToTraining: () => void
|
|
}
|
|
|
|
function getConfidenceColor(confidence: number) {
|
|
if (confidence >= 0.9) return 'bg-green-500'
|
|
if (confidence >= 0.7) return 'bg-yellow-500'
|
|
return 'bg-red-500'
|
|
}
|
|
|
|
export function TabTest({
|
|
ocrResult,
|
|
ocrLoading,
|
|
imagePreview,
|
|
uploadedImage,
|
|
settings,
|
|
showHeatmap,
|
|
onToggleHeatmap,
|
|
onFileUpload,
|
|
onManualOCR,
|
|
onClearImage,
|
|
onSendToTraining,
|
|
}: TabTestProps) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* OCR Test */}
|
|
<div className="bg-white rounded-xl shadow-sm border p-6">
|
|
<h2 className="text-lg font-semibold text-slate-900 mb-4">OCR Test</h2>
|
|
<p className="text-sm text-slate-500 mb-4">
|
|
Teste die Handschrifterkennung mit einem eigenen Bild. Das Ergebnis zeigt
|
|
den erkannten Text, Konfidenz und Verarbeitungszeit.
|
|
{settings.livePreview && (
|
|
<span className="text-purple-600 ml-1">(Live-Vorschau aktiv)</span>
|
|
)}
|
|
</p>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Upload Area */}
|
|
<UploadArea
|
|
imagePreview={imagePreview}
|
|
uploadedImage={uploadedImage}
|
|
ocrLoading={ocrLoading}
|
|
livePreview={settings.livePreview}
|
|
onFileUpload={onFileUpload}
|
|
onManualOCR={onManualOCR}
|
|
onClearImage={onClearImage}
|
|
/>
|
|
|
|
{/* Results Area */}
|
|
<ResultsArea
|
|
ocrResult={ocrResult}
|
|
ocrLoading={ocrLoading}
|
|
onSendToTraining={onSendToTraining}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Confidence Heatmap */}
|
|
{imagePreview && ocrResult && ocrResult.confidence > 0 && (
|
|
<div className="bg-white rounded-xl shadow-sm border p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-slate-900">Konfidenz-Visualisierung</h2>
|
|
<button
|
|
onClick={onToggleHeatmap}
|
|
className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
|
|
showHeatmap
|
|
? 'bg-purple-600 text-white'
|
|
: 'bg-slate-200 text-slate-700 hover:bg-slate-300'
|
|
}`}
|
|
>
|
|
{showHeatmap ? 'Heatmap verbergen' : 'Heatmap anzeigen'}
|
|
</button>
|
|
</div>
|
|
{showHeatmap && (
|
|
<ConfidenceHeatmap
|
|
imageSrc={imagePreview}
|
|
text={ocrResult.text}
|
|
confidence={ocrResult.confidence}
|
|
wordBoxes={ocrResult.word_boxes?.map(w => ({
|
|
text: w.text,
|
|
confidence: w.confidence,
|
|
bbox: w.bbox as [number, number, number, number]
|
|
})) || []}
|
|
charConfidences={ocrResult.char_confidences || []}
|
|
showLegend={true}
|
|
toggleable={true}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Confidence Interpretation */}
|
|
<div className="bg-white rounded-xl shadow-sm border p-6">
|
|
<h2 className="text-lg font-semibold text-slate-900 mb-4">Konfidenz-Interpretation</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<div className="text-green-700 font-medium">90-100%</div>
|
|
<div className="text-sm text-slate-600 mt-1">Sehr hohe Sicherheit - Text kann direkt uebernommen werden</div>
|
|
</div>
|
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
|
<div className="text-yellow-700 font-medium">70-90%</div>
|
|
<div className="text-sm text-slate-600 mt-1">Gute Sicherheit - manuelle Ueberpruefung empfohlen</div>
|
|
</div>
|
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
<div className="text-red-700 font-medium">< 70%</div>
|
|
<div className="text-sm text-slate-600 mt-1">Niedrige Sicherheit - manuelle Eingabe erforderlich</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Sub-components */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
interface UploadAreaProps {
|
|
imagePreview: string | null
|
|
uploadedImage: File | null
|
|
ocrLoading: boolean
|
|
livePreview: boolean
|
|
onFileUpload: (file: File) => void
|
|
onManualOCR: () => void
|
|
onClearImage: () => void
|
|
}
|
|
|
|
function UploadArea({ imagePreview, uploadedImage, ocrLoading, livePreview, onFileUpload, onManualOCR, onClearImage }: UploadAreaProps) {
|
|
return (
|
|
<div>
|
|
<div
|
|
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-all ${
|
|
imagePreview
|
|
? 'border-purple-500 bg-purple-50'
|
|
: 'border-slate-300 hover:border-purple-500'
|
|
}`}
|
|
onClick={() => document.getElementById('ocr-file-input')?.click()}
|
|
onDragOver={(e) => { e.preventDefault(); e.currentTarget.classList.add('border-purple-500', 'bg-purple-50') }}
|
|
onDragLeave={(e) => { e.currentTarget.classList.remove('border-purple-500', 'bg-purple-50') }}
|
|
onDrop={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
e.currentTarget.classList.remove('border-purple-500', 'bg-purple-50')
|
|
const file = e.dataTransfer.files[0]
|
|
if (file?.type.startsWith('image/')) onFileUpload(file)
|
|
}}
|
|
>
|
|
{imagePreview ? (
|
|
<div className="relative">
|
|
<img
|
|
src={imagePreview}
|
|
alt="Hochgeladenes Bild"
|
|
className="max-h-64 mx-auto rounded-lg shadow-sm"
|
|
/>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onClearImage()
|
|
}}
|
|
className="absolute top-2 right-2 p-1 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors"
|
|
title="Bild entfernen (Escape)"
|
|
>
|
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="text-4xl mb-2">📄</div>
|
|
<div className="text-slate-700">Bild hierher ziehen oder klicken zum Hochladen</div>
|
|
<div className="text-xs text-slate-400 mt-1">PNG, JPG - Handgeschriebener Text</div>
|
|
<div className="text-xs text-purple-500 mt-2">
|
|
oder <kbd className="px-1.5 py-0.5 bg-purple-100 rounded font-mono">Ctrl+V</kbd> zum Einfuegen
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
<input
|
|
type="file"
|
|
id="ocr-file-input"
|
|
accept="image/*"
|
|
className="hidden"
|
|
onChange={(e) => {
|
|
const file = e.target.files?.[0]
|
|
if (file) onFileUpload(file)
|
|
}}
|
|
/>
|
|
|
|
{uploadedImage && !livePreview && (
|
|
<button
|
|
onClick={onManualOCR}
|
|
disabled={ocrLoading}
|
|
className="w-full mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-slate-300 text-white rounded-lg text-sm font-medium transition-colors"
|
|
>
|
|
{ocrLoading ? (
|
|
<span className="flex items-center justify-center gap-2">
|
|
<SkeletonDots />
|
|
Analysiere...
|
|
</span>
|
|
) : (
|
|
'OCR starten (Ctrl+Enter)'
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface ResultsAreaProps {
|
|
ocrResult: OCRResult | null
|
|
ocrLoading: boolean
|
|
onSendToTraining: () => void
|
|
}
|
|
|
|
function ResultsArea({ ocrResult, ocrLoading, onSendToTraining }: ResultsAreaProps) {
|
|
if (ocrLoading) return <SkeletonOCRResult />
|
|
|
|
if (!ocrResult) {
|
|
return (
|
|
<div className="bg-slate-50 rounded-lg p-8 text-center text-slate-400">
|
|
<div className="text-4xl mb-2">🔍</div>
|
|
<div>Lade ein Bild hoch um die Erkennung zu testen</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="bg-slate-50 rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-sm font-medium text-slate-700">Erkannter Text:</h3>
|
|
<div className={`px-2 py-1 rounded-full text-xs font-medium ${
|
|
ocrResult.confidence >= 0.9 ? 'bg-green-100 text-green-700' :
|
|
ocrResult.confidence >= 0.7 ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-red-100 text-red-700'
|
|
}`}>
|
|
{(ocrResult.confidence * 100).toFixed(0)}% Konfidenz
|
|
</div>
|
|
</div>
|
|
<pre className="bg-white border p-3 rounded text-sm text-slate-900 whitespace-pre-wrap max-h-48 overflow-y-auto">
|
|
{ocrResult.text || '(Kein Text erkannt)'}
|
|
</pre>
|
|
|
|
{/* Confidence bar */}
|
|
<div className="mt-3 mb-3">
|
|
<div className="h-2 bg-slate-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full transition-all duration-500 ${getConfidenceColor(ocrResult.confidence)}`}
|
|
style={{ width: `${ocrResult.confidence * 100}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div className="bg-white border rounded p-2">
|
|
<div className="text-slate-500 text-xs">Konfidenz</div>
|
|
<div className="text-slate-900 font-medium">{(ocrResult.confidence * 100).toFixed(1)}%</div>
|
|
</div>
|
|
<div className="bg-white border rounded p-2">
|
|
<div className="text-slate-500 text-xs">Verarbeitungszeit</div>
|
|
<div className="text-slate-900 font-medium">{ocrResult.processing_time_ms}ms</div>
|
|
</div>
|
|
<div className="bg-white border rounded p-2">
|
|
<div className="text-slate-500 text-xs">Modell</div>
|
|
<div className="text-slate-900 font-medium">{ocrResult.model || 'TrOCR'}</div>
|
|
</div>
|
|
<div className="bg-white border rounded p-2">
|
|
<div className="text-slate-500 text-xs">LoRA Adapter</div>
|
|
<div className="text-slate-900 font-medium">{ocrResult.has_lora_adapter ? 'Ja' : 'Nein'}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{ocrResult.confidence < 0.9 && (
|
|
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
<p className="text-sm text-blue-800 mb-2">
|
|
Die Erkennung koennte verbessert werden! Moechtest du dieses Beispiel zum Training hinzufuegen?
|
|
</p>
|
|
<button
|
|
onClick={onSendToTraining}
|
|
className="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm transition-colors"
|
|
>
|
|
Als Trainingsbeispiel hinzufuegen
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|