Add interactive learning modules MVP (Phases 1-3.1)
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
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>
This commit is contained in:
157
studio-v2/app/vocab-worksheet/components/SpreadsheetTab.tsx
Normal file
157
studio-v2/app/vocab-worksheet/components/SpreadsheetTab.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* SpreadsheetTab — Fortune Sheet editor for vocabulary data.
|
||||
*
|
||||
* Converts VocabularyEntry[] into a Fortune Sheet workbook
|
||||
* where users can edit vocabulary in a familiar Excel-like UI.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import type { VocabWorksheetHook } from '../types'
|
||||
|
||||
const Workbook = dynamic(
|
||||
() => import('@fortune-sheet/react').then((m) => m.Workbook),
|
||||
{ ssr: false, loading: () => <div className="py-8 text-center text-sm text-gray-400">Spreadsheet wird geladen...</div> },
|
||||
)
|
||||
|
||||
import '@fortune-sheet/react/dist/index.css'
|
||||
|
||||
/** Convert VocabularyEntry[] to Fortune Sheet sheet data */
|
||||
function vocabToSheet(vocabulary: VocabWorksheetHook['vocabulary']) {
|
||||
const headers = ['Englisch', 'Deutsch', 'Beispielsatz', 'Wortart', 'Seite']
|
||||
const numCols = headers.length
|
||||
const numRows = vocabulary.length + 1 // +1 for header
|
||||
|
||||
const celldata: any[] = []
|
||||
|
||||
// Header row
|
||||
headers.forEach((label, c) => {
|
||||
celldata.push({
|
||||
r: 0,
|
||||
c,
|
||||
v: { v: label, m: label, bl: 1, bg: '#f0f4ff', fc: '#1e293b' },
|
||||
})
|
||||
})
|
||||
|
||||
// Data rows
|
||||
vocabulary.forEach((entry, idx) => {
|
||||
const r = idx + 1
|
||||
celldata.push({ r, c: 0, v: { v: entry.english, m: entry.english } })
|
||||
celldata.push({ r, c: 1, v: { v: entry.german, m: entry.german } })
|
||||
celldata.push({ r, c: 2, v: { v: entry.example_sentence || '', m: entry.example_sentence || '' } })
|
||||
celldata.push({ r, c: 3, v: { v: entry.word_type || '', m: entry.word_type || '' } })
|
||||
celldata.push({ r, c: 4, v: { v: entry.source_page != null ? String(entry.source_page) : '', m: entry.source_page != null ? String(entry.source_page) : '' } })
|
||||
})
|
||||
|
||||
// Column widths
|
||||
const columnlen: Record<string, number> = {
|
||||
'0': 180, // Englisch
|
||||
'1': 180, // Deutsch
|
||||
'2': 280, // Beispielsatz
|
||||
'3': 100, // Wortart
|
||||
'4': 60, // Seite
|
||||
}
|
||||
|
||||
// Row heights
|
||||
const rowlen: Record<string, number> = {}
|
||||
rowlen['0'] = 28 // header
|
||||
|
||||
// Borders: light grid
|
||||
const borderInfo = numRows > 0 && numCols > 0 ? [{
|
||||
rangeType: 'range',
|
||||
borderType: 'border-all',
|
||||
color: '#e5e7eb',
|
||||
style: 1,
|
||||
range: [{ row: [0, numRows - 1], column: [0, numCols - 1] }],
|
||||
}] : []
|
||||
|
||||
return {
|
||||
name: 'Vokabeln',
|
||||
id: 'vocab_sheet',
|
||||
celldata,
|
||||
row: numRows,
|
||||
column: numCols,
|
||||
status: 1,
|
||||
config: {
|
||||
columnlen,
|
||||
rowlen,
|
||||
borderInfo,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function SpreadsheetTab({ h }: { h: VocabWorksheetHook }) {
|
||||
const { isDark, glassCard, vocabulary } = h
|
||||
|
||||
const sheets = useMemo(() => {
|
||||
if (!vocabulary || vocabulary.length === 0) return []
|
||||
return [vocabToSheet(vocabulary)]
|
||||
}, [vocabulary])
|
||||
|
||||
const estimatedHeight = Math.max(500, (vocabulary.length + 2) * 26 + 80)
|
||||
|
||||
const handleSaveFromSheet = useCallback(async () => {
|
||||
await h.saveVocabulary()
|
||||
}, [h])
|
||||
|
||||
if (vocabulary.length === 0) {
|
||||
return (
|
||||
<div className={`${glassCard} rounded-2xl p-6`}>
|
||||
<p className={`text-center py-12 ${isDark ? 'text-white/60' : 'text-slate-500'}`}>
|
||||
Keine Vokabeln vorhanden. Bitte zuerst Seiten verarbeiten.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${glassCard} rounded-2xl p-4`}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className={`text-lg font-semibold ${isDark ? 'text-white' : 'text-slate-900'}`}>
|
||||
Spreadsheet-Editor
|
||||
</h2>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`text-sm ${isDark ? 'text-white/50' : 'text-slate-500'}`}>
|
||||
{vocabulary.length} Vokabeln
|
||||
</span>
|
||||
<button
|
||||
onClick={handleSaveFromSheet}
|
||||
className="px-4 py-2 rounded-xl text-sm font-medium bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:shadow-lg transition-all"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="rounded-xl overflow-hidden border"
|
||||
style={{
|
||||
borderColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
|
||||
}}
|
||||
>
|
||||
{sheets.length > 0 && (
|
||||
<div style={{ width: '100%', height: `${estimatedHeight}px` }}>
|
||||
<Workbook
|
||||
data={sheets}
|
||||
lang="en"
|
||||
showToolbar
|
||||
showFormulaBar={false}
|
||||
showSheetTabs={false}
|
||||
toolbarItems={[
|
||||
'undo', 'redo', '|',
|
||||
'font-bold', 'font-italic', 'font-strikethrough', '|',
|
||||
'font-color', 'background', '|',
|
||||
'font-size', '|',
|
||||
'horizontal-align', 'vertical-align', '|',
|
||||
'text-wrap', '|',
|
||||
'border',
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user