'use client' import { useState, useRef, useEffect } from 'react' import { useTheme } from '@/lib/ThemeContext' import { useLanguage } from '@/lib/LanguageContext' import { useActivity } from '@/lib/ActivityContext' import type { UploadedFile } from '@/components/QRCodeUpload' import type { VocabularyEntry, ExtraColumn, Session, StoredDocument, OcrPrompts, TabId, WorksheetType, WorksheetFormat, IpaMode, SyllableMode, VocabWorksheetHook, } from './types' import { getApiBase, DOCUMENTS_KEY, OCR_PROMPTS_KEY, SESSION_ID_KEY, defaultOcrPrompts, formatFileSize, } from './constants' import { startSessionFlow, resumeSessionFlow } from './useSessionHandlers' import { processSinglePage, reprocessPagesFlow } from './usePageProcessing' export function useVocabWorksheet(): VocabWorksheetHook { const { isDark } = useTheme() const { t } = useLanguage() const { startActivity, completeActivity } = useActivity() const [mounted, setMounted] = useState(false) // Tab state const [activeTab, setActiveTab] = useState('upload') // Session state const [session, setSession] = useState(null) const [sessionName, setSessionName] = useState('') const [isCreatingSession, setIsCreatingSession] = useState(false) const [error, setError] = useState(null) const [extractionStatus, setExtractionStatus] = useState('') // Existing sessions const [existingSessions, setExistingSessions] = useState([]) const [isLoadingSessions, setIsLoadingSessions] = useState(true) // Documents const [storedDocuments, setStoredDocuments] = useState([]) const [selectedDocumentId, setSelectedDocumentId] = useState(null) // Direct file const [directFile, setDirectFile] = useState(null) const [directFilePreview, setDirectFilePreview] = useState(null) const [showFullPreview, setShowFullPreview] = useState(false) const directFileInputRef = useRef(null) // PDF pages const [pdfPageCount, setPdfPageCount] = useState(0) const [selectedPages, setSelectedPages] = useState([]) const [pagesThumbnails, setPagesThumbnails] = useState([]) const [isLoadingThumbnails, setIsLoadingThumbnails] = useState(false) const [excludedPages, setExcludedPages] = useState([]) // Extra columns const [pageExtraColumns, setPageExtraColumns] = useState>({}) // Upload const [uploadedImage, setUploadedImage] = useState(null) const [isExtracting, setIsExtracting] = useState(false) const fileInputRef = useRef(null) // Vocabulary const [vocabulary, setVocabulary] = useState([]) // Worksheet const [selectedTypes, setSelectedTypes] = useState(['en_to_de']) const [worksheetTitle, setWorksheetTitle] = useState('') const [includeSolutions, setIncludeSolutions] = useState(true) const [lineHeight, setLineHeight] = useState('normal') const [selectedFormat, setSelectedFormat] = useState('standard') const [ipaMode, setIpaMode] = useState('none') const [syllableMode, setSyllableMode] = useState('none') // Export const [worksheetId, setWorksheetId] = useState(null) const [isGenerating, setIsGenerating] = useState(false) // Processing const [processingErrors, setProcessingErrors] = useState([]) const [successfulPages, setSuccessfulPages] = useState([]) const [failedPages, setFailedPages] = useState([]) const [currentlyProcessingPage, setCurrentlyProcessingPage] = useState(null) const [processingQueue, setProcessingQueue] = useState([]) // OCR Settings const [ocrPrompts, setOcrPrompts] = useState(defaultOcrPrompts) const [showSettings, setShowSettings] = useState(false) const [ocrEnhance, setOcrEnhance] = useState(true) const [ocrMaxCols, setOcrMaxCols] = useState(3) const [ocrMinConf, setOcrMinConf] = useState(0) // QR const [showQRModal, setShowQRModal] = useState(false) const [uploadSessionId, setUploadSessionId] = useState('') const [mobileUploadedFiles, setMobileUploadedFiles] = useState([]) const [selectedMobileFile, setSelectedMobileFile] = useState(null) // OCR Comparison const [showOcrComparison, setShowOcrComparison] = useState(false) const [ocrComparePageIndex, setOcrComparePageIndex] = useState(null) const [ocrCompareResult, setOcrCompareResult] = useState(null) const [isComparingOcr, setIsComparingOcr] = useState(false) const [ocrCompareError, setOcrCompareError] = useState(null) // --- Effects --- useEffect(() => { setMounted(true) let sid = localStorage.getItem(SESSION_ID_KEY) if (!sid) { sid = `vocab-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; localStorage.setItem(SESSION_ID_KEY, sid) } setUploadSessionId(sid) }, []) useEffect(() => { if (!mounted) return const stored = localStorage.getItem(OCR_PROMPTS_KEY) if (stored) { try { setOcrPrompts({ ...defaultOcrPrompts, ...JSON.parse(stored) }) } catch {} } }, [mounted]) useEffect(() => { if (!mounted) return const stored = localStorage.getItem(DOCUMENTS_KEY) if (stored) { try { setStoredDocuments(JSON.parse(stored).filter((d: StoredDocument) => d.type?.startsWith('image/') || d.type === 'application/pdf')) } catch {} } }, [mounted]) useEffect(() => { if (!mounted) return ;(async () => { try { const res = await fetch(`${getApiBase()}/api/v1/vocab/sessions`) if (res.ok) setExistingSessions(await res.json()) } catch {} finally { setIsLoadingSessions(false) } })() }, [mounted]) // --- Glassmorphism styles --- const glassCard = isDark ? 'backdrop-blur-xl bg-white/10 border border-white/20' : 'backdrop-blur-xl bg-white/70 border border-black/10' const glassInput = isDark ? 'bg-white/10 border-white/20 text-white placeholder-white/40 focus:border-purple-400' : 'bg-white/50 border-black/10 text-slate-900 placeholder-slate-400 focus:border-purple-500' // --- Handlers --- const saveOcrPrompts = (prompts: OcrPrompts) => { setOcrPrompts(prompts); localStorage.setItem(OCR_PROMPTS_KEY, JSON.stringify(prompts)) } const handleDirectFileSelect = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return setDirectFile(file); setSelectedDocumentId(null); setSelectedMobileFile(null) if (file.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (ev) => setDirectFilePreview(ev.target?.result as string); reader.readAsDataURL(file) } else if (file.type === 'application/pdf') { setDirectFilePreview(URL.createObjectURL(file)) } else { setDirectFilePreview(null) } } const startSession = async () => { if (!sessionName.trim()) { setError('Bitte geben Sie einen Namen fuer die Session ein.'); return } if (!selectedDocumentId && !directFile && !selectedMobileFile) { setError('Bitte waehlen Sie ein Dokument aus oder laden Sie eine Datei hoch.'); return } setIsCreatingSession(true) try { await startSessionFlow({ sessionName, selectedDocumentId, directFile, selectedMobileFile, storedDocuments, ocrPrompts, startActivity, setSession, setWorksheetTitle, setExtractionStatus, setPdfPageCount, setSelectedPages, setPagesThumbnails, setIsLoadingThumbnails, setVocabulary, setActiveTab, setError, }) } catch (error) { setError(error instanceof Error ? error.message : 'Ein Fehler ist aufgetreten') setExtractionStatus(''); setSession(null) } finally { setIsCreatingSession(false) } } const processSelectedPages = async () => { if (!session || selectedPages.length === 0) return const pagesToProcess = [...selectedPages].sort((a, b) => a - b) setIsExtracting(true); setProcessingErrors([]); setSuccessfulPages([]); setFailedPages([]) setProcessingQueue(pagesToProcess); setVocabulary([]); setActiveTab('vocabulary') const errors: string[] = []; const successful: number[] = []; const failed: number[] = [] for (let i = 0; i < pagesToProcess.length; i++) { const pageIndex = pagesToProcess[i] setCurrentlyProcessingPage(pageIndex + 1) setExtractionStatus(`Verarbeite Seite ${pageIndex + 1} von ${pagesToProcess.length}... (kann 30-60 Sekunden dauern)`) const result = await processSinglePage(session.id, pageIndex, ipaMode, syllableMode, ocrPrompts, ocrEnhance, ocrMaxCols, ocrMinConf) if (result.success) { successful.push(pageIndex + 1); setSuccessfulPages([...successful]); setVocabulary(prev => [...prev, ...result.vocabulary]) const qi = result.scanQuality ? ` | Qualitaet: ${result.scanQuality.quality_pct}%${result.scanQuality.is_degraded ? ' (degradiert!)' : ''}` : '' setExtractionStatus(`Seite ${pageIndex + 1} fertig: ${result.vocabulary.length} Vokabeln gefunden${qi}`) } else { failed.push(pageIndex + 1); setFailedPages([...failed]) if (result.error) { errors.push(result.error); setProcessingErrors([...errors]) } setExtractionStatus(`Seite ${pageIndex + 1} fehlgeschlagen`) } await new Promise(r => setTimeout(r, 500)) } setCurrentlyProcessingPage(null); setProcessingQueue([]); setIsExtracting(false) if (successful.length === pagesToProcess.length) setExtractionStatus(`Fertig! Alle ${successful.length} Seiten verarbeitet.`) else if (successful.length > 0) setExtractionStatus(`${successful.length} von ${pagesToProcess.length} Seiten verarbeitet. ${failed.length} fehlgeschlagen.`) else setExtractionStatus(`Alle Seiten fehlgeschlagen.`) // Reload thumbnails for processed pages if (successful.length > 0 && session) { const API_BASE = getApiBase(); const updatedThumbs = [...pagesThumbnails] for (const pageNum of successful) { const idx = pageNum - 1 try { const res = await fetch(`${API_BASE}/api/v1/vocab/sessions/${session.id}/pdf-thumbnail/${idx}?hires=true&t=${Date.now()}`) if (res.ok) { if (updatedThumbs[idx]) URL.revokeObjectURL(updatedThumbs[idx]); updatedThumbs[idx] = URL.createObjectURL(await res.blob()) } } catch {} } setPagesThumbnails(updatedThumbs) } setSession(prev => prev ? { ...prev, status: 'extracted' } : null) } const togglePageSelection = (i: number) => { setSelectedPages(p => p.includes(i) ? p.filter(x => x !== i) : [...p, i].sort((a, b) => a - b)) } const selectAllPages = () => setSelectedPages(Array.from({ length: pdfPageCount }, (_, i) => i).filter(p => !excludedPages.includes(p))) const selectNoPages = () => setSelectedPages([]) const excludePage = (i: number, e: React.MouseEvent) => { e.stopPropagation(); setExcludedPages(p => [...p, i]); setSelectedPages(p => p.filter(x => x !== i)) } const restoreExcludedPages = () => setExcludedPages([]) const runOcrComparison = async (pageIndex: number) => { if (!session) return setOcrComparePageIndex(pageIndex); setShowOcrComparison(true); setIsComparingOcr(true); setOcrCompareError(null); setOcrCompareResult(null) try { const res = await fetch(`${getApiBase()}/api/v1/vocab/sessions/${session.id}/compare-ocr/${pageIndex}`, { method: 'POST' }) if (!res.ok) throw new Error(`HTTP ${res.status}`) setOcrCompareResult(await res.json()) } catch (e) { setOcrCompareError(e instanceof Error ? e.message : 'Vergleich fehlgeschlagen') } finally { setIsComparingOcr(false) } } const updateVocabularyEntry = (id: string, field: string, value: string) => { setVocabulary(prev => prev.map(v => { if (v.id !== id) return v if (field === 'english' || field === 'german' || field === 'example_sentence' || field === 'word_type') return { ...v, [field]: value } return { ...v, extras: { ...(v.extras || {}), [field]: value } } })) } const addExtraColumn = (sourcePage: number) => { const label = prompt('Spaltenname:'); if (!label || !label.trim()) return const key = `extra_${Date.now()}` setPageExtraColumns(prev => ({ ...prev, [sourcePage]: [...(prev[sourcePage] || []), { key, label: label.trim() }] })) } const removeExtraColumn = (sourcePage: number, key: string) => { setPageExtraColumns(prev => ({ ...prev, [sourcePage]: (prev[sourcePage] || []).filter(c => c.key !== key) })) setVocabulary(prev => prev.map(v => { if (!v.extras || !(key in v.extras)) return v; const { [key]: _, ...rest } = v.extras; return { ...v, extras: rest } })) } const getExtraColumnsForPage = (sourcePage: number): ExtraColumn[] => [...(pageExtraColumns[0] || []), ...(pageExtraColumns[sourcePage] || [])] const getAllExtraColumns = (): ExtraColumn[] => { const seen = new Set(); const result: ExtraColumn[] = [] for (const cols of Object.values(pageExtraColumns)) for (const col of cols) { if (!seen.has(col.key)) { seen.add(col.key); result.push(col) } } return result } const deleteVocabularyEntry = (id: string) => setVocabulary(prev => prev.filter(v => v.id !== id)) const toggleVocabularySelection = (id: string) => setVocabulary(prev => prev.map(v => v.id === id ? { ...v, selected: !v.selected } : v)) const toggleAllSelection = () => { const all = vocabulary.every(v => v.selected); setVocabulary(prev => prev.map(v => ({ ...v, selected: !all }))) } const addVocabularyEntry = (atIndex?: number) => { const ne: VocabularyEntry = { id: `new-${Date.now()}`, english: '', german: '', example_sentence: '', selected: true } setVocabulary(prev => { if (atIndex === undefined) return [...prev, ne]; const nl = [...prev]; nl.splice(atIndex, 0, ne); return nl }) } const saveVocabulary = async () => { if (!session) return try { await fetch(`${getApiBase()}/api/v1/vocab/sessions/${session.id}/vocabulary`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vocabulary }) }) } catch (e) { console.error('Failed to save vocabulary:', e) } } const generateWorksheet = async () => { if (!session) return; if (selectedFormat === 'standard' && selectedTypes.length === 0) return setIsGenerating(true) try { await saveVocabulary() const API_BASE = getApiBase() const endpoint = selectedFormat === 'nru' ? 'generate-nru' : 'generate' const body = selectedFormat === 'nru' ? { title: worksheetTitle || session.name, include_solutions: includeSolutions } : { worksheet_types: selectedTypes, title: worksheetTitle || session.name, include_solutions: includeSolutions, line_height: lineHeight } const res = await fetch(`${API_BASE}/api/v1/vocab/sessions/${session.id}/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (res.ok) { const data = await res.json(); setWorksheetId(data.worksheet_id || data.id); setActiveTab('export'); completeActivity({ vocabCount: vocabulary.length }) } } catch (e) { console.error('Failed to generate worksheet:', e) } finally { setIsGenerating(false) } } const downloadPDF = (type: 'worksheet' | 'solution') => { if (!worksheetId) return window.open(`${getApiBase()}/api/v1/vocab/worksheets/${worksheetId}/${type === 'worksheet' ? 'pdf' : 'solution'}`, '_blank') } const toggleWorksheetType = (type: WorksheetType) => setSelectedTypes(prev => prev.includes(type) ? prev.filter(t => t !== type) : [...prev, type]) const resumeSession = async (existingSession: Session) => { setError(null); setExtractionStatus('Session wird geladen...') try { await resumeSessionFlow(existingSession, setSession, setWorksheetTitle, setVocabulary, setActiveTab, setExtractionStatus) } catch (e) { setError(e instanceof Error ? e.message : 'Fehler beim Laden der Session'); setExtractionStatus('') } } const resetSession = async () => { setSession(null); setSessionName(''); setVocabulary([]); setUploadedImage(null); setWorksheetId(null) setSelectedDocumentId(null); setDirectFile(null); setDirectFilePreview(null); setShowFullPreview(false) setPdfPageCount(0); setSelectedPages([]); setPagesThumbnails([]); setExcludedPages([]) setActiveTab('upload'); setError(null); setExtractionStatus('') try { const res = await fetch(`${getApiBase()}/api/v1/vocab/sessions`); if (res.ok) setExistingSessions(await res.json()) } catch {} } const deleteSession = async (sessionId: string, e: React.MouseEvent) => { e.stopPropagation() if (!confirm('Session wirklich loeschen? Diese Aktion kann nicht rueckgaengig gemacht werden.')) return try { const res = await fetch(`${getApiBase()}/api/v1/vocab/sessions/${sessionId}`, { method: 'DELETE' }); if (res.ok) setExistingSessions(prev => prev.filter(s => s.id !== sessionId)) } catch {} } const reprocessPages = (ipa: IpaMode, syllable: SyllableMode) => { if (!session) return let pages: number[] if (successfulPages.length > 0) pages = successfulPages.map(p => p - 1) else if (vocabulary.length > 0) pages = [...new Set(vocabulary.map(v => (v.source_page || 1) - 1))].sort((a, b) => a - b) else if (selectedPages.length > 0) pages = [...selectedPages] else pages = [0] if (pages.length === 0) return setIsExtracting(true); setExtractionStatus('Verarbeite mit neuen Einstellungen...') ;(async () => { const { vocabulary: allVocab, qualityInfo } = await reprocessPagesFlow( session.id, pages, ipa, syllable, ocrPrompts, ocrEnhance, ocrMaxCols, ocrMinConf, setExtractionStatus ) setVocabulary(allVocab); setIsExtracting(false) setExtractionStatus(`${allVocab.length} Vokabeln mit neuen Einstellungen${qualityInfo}`) })() } return { mounted, isDark, glassCard, glassInput, activeTab, setActiveTab, session, sessionName, setSessionName, isCreatingSession, error, setError, extractionStatus, existingSessions, isLoadingSessions, storedDocuments, selectedDocumentId, setSelectedDocumentId, directFile, setDirectFile, directFilePreview, showFullPreview, setShowFullPreview, directFileInputRef, pdfPageCount, selectedPages, pagesThumbnails, isLoadingThumbnails, excludedPages, pageExtraColumns, uploadedImage, isExtracting, vocabulary, selectedTypes, worksheetTitle, setWorksheetTitle, includeSolutions, setIncludeSolutions, lineHeight, setLineHeight, selectedFormat, setSelectedFormat, ipaMode, setIpaMode, syllableMode, setSyllableMode, worksheetId, isGenerating, processingErrors, successfulPages, failedPages, currentlyProcessingPage, ocrPrompts, showSettings, setShowSettings, showQRModal, setShowQRModal, uploadSessionId, mobileUploadedFiles, selectedMobileFile, setSelectedMobileFile, setMobileUploadedFiles, showOcrComparison, setShowOcrComparison, ocrComparePageIndex, ocrCompareResult, isComparingOcr, ocrCompareError, handleDirectFileSelect, startSession, processSelectedPages, togglePageSelection, selectAllPages, selectNoPages, excludePage, restoreExcludedPages, runOcrComparison, updateVocabularyEntry, addExtraColumn, removeExtraColumn, getExtraColumnsForPage, getAllExtraColumns, deleteVocabularyEntry, toggleVocabularySelection, toggleAllSelection, addVocabularyEntry, saveVocabulary, generateWorksheet, downloadPDF, toggleWorksheetType, resumeSession, resetSession, deleteSession, saveOcrPrompts, formatFileSize, reprocessPages, } }