Files
breakpilot-lehrer/studio-v2/app/vocab-worksheet/page.tsx
Benjamin Admin 909d0729f6
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
Add SmartSpellChecker + refactor vocab-worksheet page.tsx
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>
2026-04-12 12:25:01 +02:00

199 lines
10 KiB
TypeScript

'use client'
import React from 'react'
import { Sidebar } from '@/components/Sidebar'
import { useVocabWorksheet } from './useVocabWorksheet'
import { UploadScreen } from './components/UploadScreen'
import { PageSelection } from './components/PageSelection'
import { VocabularyTab } from './components/VocabularyTab'
import { WorksheetTab } from './components/WorksheetTab'
import { ExportTab } from './components/ExportTab'
import { OcrSettingsPanel } from './components/OcrSettingsPanel'
import { FullscreenPreview } from './components/FullscreenPreview'
import { QRCodeModal } from './components/QRCodeModal'
import { OcrComparisonModal } from './components/OcrComparisonModal'
import type { TabId } from './types'
export default function VocabWorksheetPage() {
const h = useVocabWorksheet()
const { isDark, glassCard, session, activeTab } = h
if (!h.mounted) {
return (
<div className="min-h-screen bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800 flex items-center justify-center">
<div className="w-8 h-8 border-4 border-purple-400 border-t-transparent rounded-full animate-spin" />
</div>
)
}
return (
<div className={`min-h-screen flex relative overflow-hidden ${
isDark
? 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-800'
: 'bg-gradient-to-br from-slate-100 via-purple-50 to-pink-100'
}`}>
{/* Animated Background Blobs */}
<div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className={`absolute -top-40 -right-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob ${
isDark ? 'bg-purple-500 opacity-70' : 'bg-purple-300 opacity-50'
}`} />
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-2000 ${
isDark ? 'bg-pink-500 opacity-70' : 'bg-pink-300 opacity-50'
}`} />
<div className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-blob animation-delay-4000 ${
isDark ? 'bg-indigo-500 opacity-70' : 'bg-indigo-300 opacity-50'
}`} />
</div>
{/* Sidebar */}
<div className="relative z-10 p-4">
<Sidebar />
</div>
{/* Main Content */}
<div className="flex-1 flex flex-col relative z-10 overflow-y-auto">
{/* Header */}
<div className={`border-b ${isDark ? 'border-white/10' : 'border-black/5'}`}>
<div className={`${glassCard} border-0 border-b`}>
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
isDark ? 'bg-purple-500/30' : 'bg-purple-200'
}`}>
<svg className={`w-6 h-6 ${isDark ? 'text-purple-300' : 'text-purple-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<div>
<h1 className={`text-xl font-bold ${isDark ? 'text-white' : 'text-slate-900'}`}>
Vokabel-Arbeitsblatt Generator
</h1>
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
Schulbuchseiten scannen KI extrahiert Vokabeln Druckfertige Arbeitsblaetter
{session && (
<span className={`ml-2 font-mono text-xs ${isDark ? 'text-white/40' : 'text-slate-400'}`}>
Session: {session.id.slice(0, 8)}
</span>
)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Settings Button */}
<button
onClick={() => h.setShowSettings(!h.showSettings)}
className={`p-2 rounded-xl transition-all ${
h.showSettings
? (isDark ? 'bg-purple-500/30 text-purple-300' : 'bg-purple-200 text-purple-700')
: (isDark ? 'hover:bg-white/10 text-white/60 hover:text-white' : 'hover:bg-black/5 text-slate-500 hover:text-slate-700')
}`}
title="OCR-Einstellungen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</button>
{/* Back to session list */}
{session && (
<button
onClick={h.resetSession}
className={`p-2 rounded-xl transition-all ${isDark ? 'hover:bg-white/10 text-white/60 hover:text-white' : 'hover:bg-black/5 text-slate-500 hover:text-slate-700'}`}
title="Zurueck zur Session-Liste"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
</button>
)}
</div>
</div>
</div>
</div>
</div>
<div className="relative z-10 w-full px-6 py-6">
{/* OCR Settings Panel */}
{h.showSettings && <OcrSettingsPanel h={h} />}
{/* Error Message */}
{h.error && (
<div className={`${glassCard} rounded-2xl p-4 mb-6 ${isDark ? 'bg-red-500/20 border-red-500/30' : 'bg-red-100/80 border-red-200'}`}>
<p className={isDark ? 'text-red-200' : 'text-red-700'}>{h.error}</p>
</div>
)}
{/* Status Message */}
{h.extractionStatus && (
<div className={`${glassCard} rounded-2xl p-4 mb-6 ${isDark ? 'bg-purple-500/20 border-purple-500/30' : 'bg-purple-100/80 border-purple-200'}`}>
{h.isCreatingSession || h.isExtracting ? (
<div className="flex items-center gap-3">
<div className={`w-5 h-5 border-2 ${isDark ? 'border-purple-300' : 'border-purple-600'} border-t-transparent rounded-full animate-spin`} />
<span className={isDark ? 'text-purple-200' : 'text-purple-700'}>{h.extractionStatus}</span>
</div>
) : (
<span className={isDark ? 'text-purple-200' : 'text-purple-700'}>{h.extractionStatus}</span>
)}
</div>
)}
{/* Tab Content */}
{!session && <UploadScreen h={h} />}
{session && activeTab === 'pages' && <PageSelection h={h} />}
{session && activeTab === 'vocabulary' && <VocabularyTab h={h} />}
{session && activeTab === 'worksheet' && <WorksheetTab h={h} />}
{session && activeTab === 'export' && <ExportTab h={h} />}
{/* Tab Navigation */}
{session && activeTab !== 'pages' && (
<div className={`mt-6 border-t pt-4 ${isDark ? 'border-white/10' : 'border-black/5'}`}>
<div className="flex justify-center gap-2">
{(['vocabulary', 'worksheet', 'export'] as TabId[]).map((tab) => (
<button
key={tab}
onClick={() => h.setActiveTab(tab)}
className={`px-4 py-2 rounded-xl text-sm font-medium transition-all ${
activeTab === tab
? 'bg-gradient-to-r from-purple-500 to-pink-500 text-white shadow-lg'
: (isDark ? 'bg-white/10 text-white/80 hover:bg-white/20' : 'bg-slate-100 text-slate-700 hover:bg-slate-200')
}`}
>
{tab === 'vocabulary' ? 'Vokabeln' : tab === 'worksheet' ? 'Arbeitsblatt' : 'Export'}
</button>
))}
</div>
</div>
)}
</div>
{/* Modals */}
{h.showFullPreview && <FullscreenPreview h={h} />}
{h.showQRModal && <QRCodeModal h={h} />}
{h.showOcrComparison && <OcrComparisonModal h={h} />}
{/* CSS for animations */}
<style jsx>{`
@keyframes blob {
0% { transform: translate(0px, 0px) scale(1); }
33% { transform: translate(30px, -50px) scale(1.1); }
66% { transform: translate(-20px, 20px) scale(0.9); }
100% { transform: translate(0px, 0px) scale(1); }
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
`}</style>
</div>
</div>
)
}