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 45s
CI / test-go-edu-search (push) Successful in 43s
CI / test-python-klausur (push) Failing after 2m51s
CI / test-python-agent-core (push) Successful in 36s
CI / test-nodejs-website (push) Successful in 37s
SmartSpellChecker (klausur-service): - Language-aware OCR post-correction without LLMs - Dual-dictionary heuristic for EN/DE language detection - Context-based a/I disambiguation via bigram lookup - Multi-digit substitution (sch00l→school) - Cross-language guard (don't false-correct DE words in EN column) - Umlaut correction (Schuler→Schüler, uber→über) - Integrated into spell_review_entries_sync() pipeline - 31 tests, 9ms/100 corrections Vocab-worksheet refactoring (studio-v2): - Split 2337-line page.tsx into 14 files - Custom hook useVocabWorksheet.ts (all state + logic) - 9 components in components/ directory - types.ts, constants.ts for shared definitions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
136 lines
6.5 KiB
TypeScript
136 lines
6.5 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import type { VocabWorksheetHook } from '../types'
|
|
|
|
export function OcrComparisonModal({ h }: { h: VocabWorksheetHook }) {
|
|
const { isDark, glassCard } = h
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
<div className={`relative w-full max-w-6xl max-h-[90vh] overflow-auto rounded-3xl ${glassCard} p-6`}>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h2 className={`text-xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
OCR-Methoden Vergleich
|
|
</h2>
|
|
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
Seite {h.ocrComparePageIndex !== null ? h.ocrComparePageIndex + 1 : '-'}
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => h.setShowOcrComparison(false)}
|
|
className={`p-2 rounded-xl ${isDark ? 'hover:bg-white/10 text-white' : 'hover:bg-black/5 text-slate-500'}`}
|
|
>
|
|
<svg className="w-6 h-6" 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>
|
|
|
|
{/* Loading State */}
|
|
{h.isComparingOcr && (
|
|
<div className="flex flex-col items-center justify-center py-12">
|
|
<div className="w-12 h-12 border-4 border-purple-500 border-t-transparent rounded-full animate-spin mb-4" />
|
|
<p className={isDark ? 'text-white/60' : 'text-slate-500'}>
|
|
Vergleiche OCR-Methoden... (kann 1-2 Minuten dauern)
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Error State */}
|
|
{h.ocrCompareError && (
|
|
<div className={`p-4 rounded-xl ${isDark ? 'bg-red-500/20 text-red-300' : 'bg-red-100 text-red-700'}`}>
|
|
Fehler: {h.ocrCompareError}
|
|
</div>
|
|
)}
|
|
|
|
{/* Results */}
|
|
{h.ocrCompareResult && !h.isComparingOcr && (
|
|
<div className="space-y-6">
|
|
{/* Method Results Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{Object.entries(h.ocrCompareResult.methods || {}).map(([key, method]: [string, any]) => (
|
|
<div
|
|
key={key}
|
|
className={`p-4 rounded-2xl ${
|
|
h.ocrCompareResult.recommendation?.best_method === key
|
|
? (isDark ? 'bg-green-500/20 border border-green-500/50' : 'bg-green-100 border border-green-300')
|
|
: (isDark ? 'bg-white/5 border border-white/10' : 'bg-white/50 border border-black/10')
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between mb-3">
|
|
<h3 className={`font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
{method.name}
|
|
</h3>
|
|
{h.ocrCompareResult.recommendation?.best_method === key && (
|
|
<span className="px-2 py-1 text-xs font-medium bg-green-500 text-white rounded-full">
|
|
Beste
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{method.success ? (
|
|
<>
|
|
<div className={`text-sm mb-2 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
<span className="font-medium">{method.vocabulary_count}</span> Vokabeln in <span className="font-medium">{method.duration_seconds}s</span>
|
|
</div>
|
|
|
|
{method.vocabulary && method.vocabulary.length > 0 && (
|
|
<div className={`max-h-48 overflow-y-auto rounded-xl p-2 ${isDark ? 'bg-black/20' : 'bg-white/50'}`}>
|
|
{method.vocabulary.slice(0, 10).map((v: any, idx: number) => (
|
|
<div key={idx} className={`text-sm py-1 border-b last:border-0 ${isDark ? 'border-white/10 text-white/80' : 'border-black/5 text-slate-700'}`}>
|
|
<span className="font-medium">{v.english}</span> = {v.german}
|
|
</div>
|
|
))}
|
|
{method.vocabulary.length > 10 && (
|
|
<div className={`text-xs mt-2 ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
|
|
+ {method.vocabulary.length - 10} weitere...
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className={`text-sm ${isDark ? 'text-red-300' : 'text-red-600'}`}>
|
|
{method.error || 'Fehler'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Comparison Summary */}
|
|
{h.ocrCompareResult.comparison && (
|
|
<div className={`p-4 rounded-2xl ${isDark ? 'bg-blue-500/20 border border-blue-500/30' : 'bg-blue-100 border border-blue-200'}`}>
|
|
<h3 className={`font-semibold mb-3 ${isDark ? 'text-blue-300' : 'text-blue-900'}`}>
|
|
Uebereinstimmung
|
|
</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span className={isDark ? 'text-blue-200' : 'text-blue-700'}>Von allen erkannt:</span>
|
|
<span className="ml-2 font-bold">{h.ocrCompareResult.comparison.found_by_all_methods?.length || 0}</span>
|
|
</div>
|
|
<div>
|
|
<span className={isDark ? 'text-blue-200' : 'text-blue-700'}>Nur teilweise:</span>
|
|
<span className="ml-2 font-bold">{h.ocrCompareResult.comparison.found_by_some_methods?.length || 0}</span>
|
|
</div>
|
|
<div>
|
|
<span className={isDark ? 'text-blue-200' : 'text-blue-700'}>Gesamt einzigartig:</span>
|
|
<span className="ml-2 font-bold">{h.ocrCompareResult.comparison.total_unique_vocabulary || 0}</span>
|
|
</div>
|
|
<div>
|
|
<span className={isDark ? 'text-blue-200' : 'text-blue-700'}>Uebereinstimmung:</span>
|
|
<span className="ml-2 font-bold">{Math.round((h.ocrCompareResult.comparison.agreement_rate || 0) * 100)}%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|