Python (6 files in klausur-service): - rbac.py (1,132 → 4), admin_api.py (1,012 → 4) - routes/eh.py (1,111 → 4), ocr_pipeline_geometry.py (1,105 → 5) Python (2 files in backend-lehrer): - unit_api.py (1,226 → 6), game_api.py (1,129 → 5) Website (6 page files): - 4x klausur-korrektur pages (1,249-1,328 LOC each) → shared components in website/components/klausur-korrektur/ (17 shared files) - companion (1,057 → 10), magic-help (1,017 → 8) All re-export barrels preserve backward compatibility. Zero import errors verified. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
181 lines
5.4 KiB
TypeScript
181 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import type { TabId, TrOCRStatus, OCRResult, TrainingExample, MagicSettings } from './types'
|
|
import { DEFAULT_SETTINGS } from './types'
|
|
|
|
export function useMagicHelp() {
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
const [status, setStatus] = useState<TrOCRStatus | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [ocrResult, setOcrResult] = useState<OCRResult | null>(null)
|
|
const [ocrLoading, setOcrLoading] = useState(false)
|
|
const [examples, setExamples] = useState<TrainingExample[]>([])
|
|
const [trainingImage, setTrainingImage] = useState<File | null>(null)
|
|
const [trainingText, setTrainingText] = useState('')
|
|
const [fineTuning, setFineTuning] = useState(false)
|
|
const [settings, setSettings] = useState<MagicSettings>(DEFAULT_SETTINGS)
|
|
const [settingsSaved, setSettingsSaved] = useState(false)
|
|
|
|
const fetchStatus = useCallback(async () => {
|
|
try {
|
|
const res = await fetch('/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/klausur/trocr/training/examples')
|
|
const data = await res.json()
|
|
setExamples(data.examples || [])
|
|
} catch (error) {
|
|
console.error('Failed to fetch examples:', error)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
fetchStatus()
|
|
fetchExamples()
|
|
// Load settings from localStorage
|
|
const saved = localStorage.getItem('magic-help-settings')
|
|
if (saved) {
|
|
try {
|
|
setSettings(JSON.parse(saved))
|
|
} catch {
|
|
// ignore parse errors
|
|
}
|
|
}
|
|
}, [fetchStatus, fetchExamples])
|
|
|
|
const handleFileUpload = async (file: File) => {
|
|
setOcrLoading(true)
|
|
setOcrResult(null)
|
|
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
try {
|
|
const res = await fetch(`/api/klausur/trocr/extract?detect_lines=${settings.autoDetectLines}`, {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
const data = await res.json()
|
|
if (data.text !== undefined) {
|
|
setOcrResult(data)
|
|
} 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)
|
|
}
|
|
}
|
|
|
|
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/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/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 getStatusBadge = () => {
|
|
if (!status) return null
|
|
switch (status.status) {
|
|
case 'available':
|
|
return <span className="px-2 py-1 text-xs font-medium rounded-full bg-green-500/20 text-green-400">Available</span>
|
|
case 'not_installed':
|
|
return <span className="px-2 py-1 text-xs font-medium rounded-full bg-red-500/20 text-red-400">Not Installed</span>
|
|
case 'error':
|
|
return <span className="px-2 py-1 text-xs font-medium rounded-full bg-yellow-500/20 text-yellow-400">Error</span>
|
|
}
|
|
}
|
|
|
|
return {
|
|
activeTab,
|
|
setActiveTab,
|
|
status,
|
|
loading,
|
|
ocrResult,
|
|
ocrLoading,
|
|
examples,
|
|
trainingImage,
|
|
setTrainingImage,
|
|
trainingText,
|
|
setTrainingText,
|
|
fineTuning,
|
|
settings,
|
|
setSettings,
|
|
settingsSaved,
|
|
fetchStatus,
|
|
handleFileUpload,
|
|
handleAddTrainingExample,
|
|
handleFineTune,
|
|
saveSettings,
|
|
getStatusBadge,
|
|
}
|
|
}
|