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 44s
CI / test-go-edu-search (push) Successful in 51s
CI / test-python-klausur (push) Failing after 2m44s
CI / test-python-agent-core (push) Successful in 33s
CI / test-nodejs-website (push) Successful in 34s
New feature: After OCR vocabulary extraction, users can generate interactive
learning modules (flashcards, quiz, type trainer) with one click.
Frontend (studio-v2):
- Fortune Sheet spreadsheet editor tab in vocab-worksheet
- "Lernmodule generieren" button in ExportTab
- /learn page with unit overview and exercise type cards
- /learn/[unitId]/flashcards — Flip-card trainer with Leitner spaced repetition
- /learn/[unitId]/quiz — Multiple choice quiz with explanations
- /learn/[unitId]/type — Type-in trainer with Levenshtein distance feedback
- AudioButton component using Web Speech API for EN+DE TTS
Backend (klausur-service):
- vocab_learn_bridge.py: Converts VocabularyEntry[] to analysis_data format
- POST /sessions/{id}/generate-learning-unit endpoint
Backend (backend-lehrer):
- generate-qa, generate-mc, generate-cloze endpoints on learning units
- get-qa/mc/cloze data retrieval endpoints
- Leitner progress update + next review items endpoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
165 lines
6.6 KiB
TypeScript
165 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useTheme } from '@/lib/ThemeContext'
|
|
import { Sidebar } from '@/components/Sidebar'
|
|
import { UnitCard } from '@/components/learn/UnitCard'
|
|
|
|
interface LearningUnit {
|
|
id: string
|
|
label: string
|
|
meta: string
|
|
title: string
|
|
topic: string | null
|
|
grade_level: string | null
|
|
status: string
|
|
vocabulary_count?: number
|
|
created_at: string
|
|
}
|
|
|
|
function getBackendUrl() {
|
|
if (typeof window === 'undefined') return 'http://localhost:8001'
|
|
const { hostname, protocol } = window.location
|
|
if (hostname === 'localhost') return 'http://localhost:8001'
|
|
return `${protocol}//${hostname}:8001`
|
|
}
|
|
|
|
export default function LearnPage() {
|
|
const { isDark } = useTheme()
|
|
const [units, setUnits] = useState<LearningUnit[]>([])
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
const glassCard = isDark
|
|
? 'bg-white/10 backdrop-blur-xl border border-white/10'
|
|
: 'bg-white/80 backdrop-blur-xl border border-black/5'
|
|
|
|
useEffect(() => {
|
|
loadUnits()
|
|
}, [])
|
|
|
|
const loadUnits = async () => {
|
|
setIsLoading(true)
|
|
try {
|
|
const resp = await fetch(`${getBackendUrl()}/api/learning-units/`)
|
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
|
|
const data = await resp.json()
|
|
setUnits(data)
|
|
} catch (err: any) {
|
|
setError(err.message)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (unitId: string) => {
|
|
try {
|
|
const resp = await fetch(`${getBackendUrl()}/api/learning-units/${unitId}`, { method: 'DELETE' })
|
|
if (resp.ok) {
|
|
setUnits((prev) => prev.filter((u) => u.id !== unitId))
|
|
}
|
|
} catch (err) {
|
|
console.error('Delete failed:', err)
|
|
}
|
|
}
|
|
|
|
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-blue-50 to-cyan-100'
|
|
}`}>
|
|
{/* 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-pulse ${
|
|
isDark ? 'bg-blue-500 opacity-50' : 'bg-blue-300 opacity-30'
|
|
}`} />
|
|
<div className={`absolute -bottom-40 -left-40 w-80 h-80 rounded-full mix-blend-multiply filter blur-3xl animate-pulse ${
|
|
isDark ? 'bg-cyan-500 opacity-50' : 'bg-cyan-300 opacity-30'
|
|
}`} style={{ animationDelay: '2s' }} />
|
|
</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={`${glassCard} border-0 border-b`}>
|
|
<div className="max-w-5xl mx-auto px-6 py-4">
|
|
<div className="flex items-center gap-4">
|
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${
|
|
isDark ? 'bg-blue-500/30' : 'bg-blue-200'
|
|
}`}>
|
|
<svg className={`w-6 h-6 ${isDark ? 'text-blue-300' : 'text-blue-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'}`}>
|
|
Meine Lernmodule
|
|
</h1>
|
|
<p className={`text-sm ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
Karteikarten, Quiz und Lueckentexte aus deinen Vokabeln
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="max-w-5xl mx-auto w-full px-6 py-6">
|
|
{isLoading && (
|
|
<div className="flex items-center justify-center py-20">
|
|
<div className={`w-8 h-8 border-4 ${isDark ? 'border-blue-400' : 'border-blue-600'} border-t-transparent rounded-full animate-spin`} />
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className={`${glassCard} rounded-2xl p-6 text-center`}>
|
|
<p className={`${isDark ? 'text-red-300' : 'text-red-600'}`}>Fehler: {error}</p>
|
|
<button onClick={loadUnits} className="mt-3 px-4 py-2 rounded-xl bg-blue-500 text-white text-sm">
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!isLoading && !error && units.length === 0 && (
|
|
<div className={`${glassCard} rounded-2xl p-12 text-center`}>
|
|
<div className={`w-16 h-16 mx-auto mb-4 rounded-2xl flex items-center justify-center ${
|
|
isDark ? 'bg-blue-500/20' : 'bg-blue-100'
|
|
}`}>
|
|
<svg className={`w-8 h-8 ${isDark ? 'text-blue-300' : 'text-blue-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>
|
|
<h3 className={`text-lg font-semibold mb-2 ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
|
Noch keine Lernmodule
|
|
</h3>
|
|
<p className={`text-sm mb-4 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
|
Scanne eine Schulbuchseite im Vokabel-Arbeitsblatt Generator und klicke "Lernmodule generieren".
|
|
</p>
|
|
<a
|
|
href="/vocab-worksheet"
|
|
className="inline-block px-6 py-3 rounded-xl bg-gradient-to-r from-blue-500 to-cyan-500 text-white font-medium hover:shadow-lg transition-all"
|
|
>
|
|
Zum Vokabel-Scanner
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
{!isLoading && units.length > 0 && (
|
|
<div className="grid gap-4">
|
|
{units.map((unit) => (
|
|
<UnitCard key={unit.id} unit={unit} isDark={isDark} glassCard={glassCard} onDelete={handleDelete} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|