'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { type TabId, type TrOCRStatus, type OCRResult, type TrainingExample, type MagicSettings, DEFAULT_SETTINGS, API_BASE, } from './types' function playSuccessSound() { try { const audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)() const oscillator = audioContext.createOscillator() const gainNode = audioContext.createGain() oscillator.connect(gainNode) gainNode.connect(audioContext.destination) oscillator.frequency.value = 800 oscillator.type = 'sine' gainNode.gain.setValueAtTime(0.1, audioContext.currentTime) gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.2) oscillator.start(audioContext.currentTime) oscillator.stop(audioContext.currentTime + 0.2) } catch { // Audio not supported, ignore } } export function useMagicHelp() { const [activeTab, setActiveTab] = useState('overview') const [status, setStatus] = useState(null) const [loading, setLoading] = useState(true) const [ocrResult, setOcrResult] = useState(null) const [ocrLoading, setOcrLoading] = useState(false) const [examples, setExamples] = useState([]) const [trainingImage, setTrainingImage] = useState(null) const [trainingText, setTrainingText] = useState('') const [fineTuning, setFineTuning] = useState(false) const [settings, setSettings] = useState(DEFAULT_SETTINGS) const [settingsSaved, setSettingsSaved] = useState(false) // Phase 1: New state for enhanced features const [globalDragActive, setGlobalDragActive] = useState(false) const [uploadedImage, setUploadedImage] = useState(null) const [imagePreview, setImagePreview] = useState(null) const [showShortcutHint, setShowShortcutHint] = useState(false) const [showHeatmap, setShowHeatmap] = useState(false) const [showTrainingDashboard, setShowTrainingDashboard] = useState(false) const debounceTimer = useRef(null) const dragCounter = useRef(0) const fetchStatus = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/klausur/trocr/status`) const data = await res.json() setStatus(data) } catch { setStatus({ status: 'error', error: 'Failed to fetch status' }) } finally { setLoading(false) } }, []) const fetchExamples = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/klausur/trocr/training/examples`) const data = await res.json() setExamples(data.examples || []) } catch (error) { console.error('Failed to fetch examples:', error) } }, []) // Phase 1: Live OCR with debounce const triggerOCR = useCallback(async (file: File) => { setOcrLoading(true) setOcrResult(null) const formData = new FormData() formData.append('file', file) try { const res = await fetch(`${API_BASE}/api/klausur/trocr/extract?detect_lines=${settings.autoDetectLines}`, { method: 'POST', body: formData, }) const data = await res.json() if (data.text !== undefined) { setOcrResult(data) if (settings.soundFeedback && data.confidence > 0.7) { playSuccessSound() } } else { setOcrResult({ text: `Error: ${data.detail || 'Unknown error'}`, confidence: 0, processing_time_ms: 0, model: '', has_lora_adapter: false }) } } catch (error) { setOcrResult({ text: `Error: ${error}`, confidence: 0, processing_time_ms: 0, model: '', has_lora_adapter: false }) } finally { setOcrLoading(false) } }, [settings.autoDetectLines, settings.soundFeedback]) // Handle file upload with live preview const handleFileUpload = useCallback((file: File) => { if (!file.type.startsWith('image/')) return setUploadedImage(file) const previewUrl = URL.createObjectURL(file) setImagePreview(previewUrl) setActiveTab('test') if (settings.livePreview) { if (debounceTimer.current) { clearTimeout(debounceTimer.current) } debounceTimer.current = setTimeout(() => { triggerOCR(file) }, 500) } }, [settings.livePreview, triggerOCR]) const handleManualOCR = () => { if (uploadedImage) { triggerOCR(uploadedImage) } } // Phase 1: Global Drag & Drop handler useEffect(() => { const handleDragEnter = (e: DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current++ if (e.dataTransfer?.types.includes('Files')) { setGlobalDragActive(true) } } const handleDragLeave = (e: DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current-- if (dragCounter.current === 0) { setGlobalDragActive(false) } } const handleDragOver = (e: DragEvent) => { e.preventDefault() e.stopPropagation() } const handleDrop = (e: DragEvent) => { e.preventDefault() e.stopPropagation() dragCounter.current = 0 setGlobalDragActive(false) const file = e.dataTransfer?.files[0] if (file?.type.startsWith('image/')) { handleFileUpload(file) } } document.addEventListener('dragenter', handleDragEnter) document.addEventListener('dragleave', handleDragLeave) document.addEventListener('dragover', handleDragOver) document.addEventListener('drop', handleDrop) return () => { document.removeEventListener('dragenter', handleDragEnter) document.removeEventListener('dragleave', handleDragLeave) document.removeEventListener('dragover', handleDragOver) document.removeEventListener('drop', handleDrop) } }, [handleFileUpload]) // Phase 1: Clipboard paste handler (Ctrl+V) useEffect(() => { const handlePaste = async (e: ClipboardEvent) => { const items = e.clipboardData?.items if (!items) return for (const item of items) { if (item.type.startsWith('image/')) { e.preventDefault() const file = item.getAsFile() if (file) { handleFileUpload(file) } break } } } document.addEventListener('paste', handlePaste) return () => document.removeEventListener('paste', handlePaste) }, [handleFileUpload]) // Phase 1: Keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === 'Enter' && uploadedImage) { e.preventDefault() handleManualOCR() } if (e.key >= '1' && e.key <= '6' && e.altKey) { e.preventDefault() const tabIndex = parseInt(e.key) - 1 const tabIds: TabId[] = ['overview', 'test', 'batch', 'training', 'architecture', 'settings'] if (tabIds[tabIndex]) { setActiveTab(tabIds[tabIndex]) } } if (e.key === 'Escape' && uploadedImage) { setUploadedImage(null) setImagePreview(null) setOcrResult(null) } if (e.key === '?') { setShowShortcutHint(prev => !prev) } } document.addEventListener('keydown', handleKeyDown) return () => document.removeEventListener('keydown', handleKeyDown) // eslint-disable-next-line react-hooks/exhaustive-deps }, [uploadedImage]) // Initial data load + settings from localStorage useEffect(() => { fetchStatus() fetchExamples() const saved = localStorage.getItem('magic-help-settings') if (saved) { try { setSettings({ ...DEFAULT_SETTINGS, ...JSON.parse(saved) }) } catch { // ignore parse errors } } }, [fetchStatus, fetchExamples]) // Cleanup preview URL useEffect(() => { return () => { if (imagePreview) { URL.revokeObjectURL(imagePreview) } } }, [imagePreview]) const handleAddTrainingExample = async () => { if (!trainingImage || !trainingText.trim()) { alert('Please provide both an image and the correct text') return } const formData = new FormData() formData.append('file', trainingImage) try { const res = await fetch(`${API_BASE}/api/klausur/trocr/training/add?ground_truth=${encodeURIComponent(trainingText)}`, { method: 'POST', body: formData, }) const data = await res.json() if (data.example_id) { alert(`Training example added! Total: ${data.total_examples}`) setTrainingImage(null) setTrainingText('') fetchStatus() fetchExamples() } else { alert(`Error: ${data.detail || 'Unknown error'}`) } } catch (error) { alert(`Error: ${error}`) } } const handleFineTune = async () => { if (!confirm('Start fine-tuning? This may take several minutes.')) return setFineTuning(true) try { const res = await fetch(`${API_BASE}/api/klausur/trocr/training/fine-tune`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ epochs: settings.epochs, learning_rate: settings.learningRate, lora_rank: settings.loraRank, lora_alpha: settings.loraAlpha, }), }) const data = await res.json() if (data.status === 'success') { alert(`Fine-tuning successful!\nExamples used: ${data.examples_used}\nEpochs: ${data.epochs}`) fetchStatus() } else { alert(`Fine-tuning failed: ${data.message}`) } } catch (error) { alert(`Error: ${error}`) } finally { setFineTuning(false) } } const saveSettings = () => { localStorage.setItem('magic-help-settings', JSON.stringify(settings)) setSettingsSaved(true) setTimeout(() => setSettingsSaved(false), 2000) } const clearUploadedImage = () => { setUploadedImage(null) setImagePreview(null) setOcrResult(null) } const sendToTraining = () => { if (uploadedImage && ocrResult) { setTrainingImage(uploadedImage) setTrainingText(ocrResult.text) setActiveTab('training') } } return { // State activeTab, setActiveTab, status, loading, ocrResult, ocrLoading, examples, trainingImage, setTrainingImage, trainingText, setTrainingText, fineTuning, settings, setSettings, settingsSaved, globalDragActive, uploadedImage, imagePreview, showShortcutHint, setShowShortcutHint, showHeatmap, setShowHeatmap, showTrainingDashboard, setShowTrainingDashboard, // Actions fetchStatus, handleFileUpload, handleManualOCR, handleAddTrainingExample, handleFineTune, saveSettings, clearUploadedImage, sendToTraining, } } export type UseMagicHelpReturn = ReturnType