feat: individual word link_to (F/A/B), edit form word management, fix picture-based suggestions
- Words in pair form now linkable individually per word (Frage/Aussage/Beide toggle) - Edit form includes full word management: view existing words with link indicator, remove/restore, add new words with link_to selector - Fix word suggestions: load from picture words (db_words_db_pictures) instead of object words (always empty) - Backend PATCH /api/directus/db-pairs/<id> handles words_add with link_to and words_remove with junction IDs - Level range 1-100 throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
55
app.py
55
app.py
@@ -1836,6 +1836,22 @@ def directus_db_object_pairs(obj_id):
|
||||
s_d, _ = _directus("GET", f"/items/db_statement/{sid}?fields=id,statement_de,level,status", token)
|
||||
if s_d.get("data"):
|
||||
statements.append(s_d["data"])
|
||||
# Wörter für jede Question laden
|
||||
for q in questions:
|
||||
qid = q["id"]
|
||||
qw_junc, _ = _directus("GET", f"/items/db_question_db_words?filter[db_question_id][_eq]={qid}&fields=id,db_words_id.id,db_words_id.titel_de,db_words_id.level&limit=100", token)
|
||||
q["words"] = [
|
||||
{"junction_id": e["id"], "word_id": e["db_words_id"]["id"], "titel_de": e["db_words_id"]["titel_de"], "level": e["db_words_id"]["level"]}
|
||||
for e in (qw_junc.get("data") or []) if isinstance(e.get("db_words_id"), dict)
|
||||
]
|
||||
# Wörter für jedes Statement laden
|
||||
for s in statements:
|
||||
sid = s["id"]
|
||||
sw_junc, _ = _directus("GET", f"/items/db_statement_db_words?filter[db_statement_id][_eq]={sid}&fields=id,db_words_id.id,db_words_id.titel_de,db_words_id.level&limit=100", token)
|
||||
s["words"] = [
|
||||
{"junction_id": e["id"], "word_id": e["db_words_id"]["id"], "titel_de": e["db_words_id"]["titel_de"], "level": e["db_words_id"]["level"]}
|
||||
for e in (sw_junc.get("data") or []) if isinstance(e.get("db_words_id"), dict)
|
||||
]
|
||||
result.append({**pair, "questions": questions, "statements": statements})
|
||||
return jsonify({"data": result})
|
||||
else:
|
||||
@@ -1865,12 +1881,14 @@ def directus_db_object_pairs(obj_id):
|
||||
for we in words:
|
||||
titel_de = (we.get("titel_de") or "").strip()
|
||||
w_level = int(we.get("level") or level)
|
||||
link_to = we.get("link_to", "both") # 'question', 'statement', 'both'
|
||||
if not titel_de:
|
||||
continue
|
||||
try:
|
||||
wid, _ = _find_or_create_db_word(titel_de, w_level, token)
|
||||
if link_to in ("statement", "both"):
|
||||
_directus("POST", "/items/db_statement_db_words", token, {"db_statement_id": stmt_id, "db_words_id": wid})
|
||||
if q_id:
|
||||
if link_to in ("question", "both") and q_id:
|
||||
_directus("POST", "/items/db_question_db_words", token, {"db_question_id": q_id, "db_words_id": wid})
|
||||
except Exception as e:
|
||||
print(f"[db_object_pairs] word error '{titel_de}': {e}")
|
||||
@@ -1953,6 +1971,41 @@ def directus_db_pair(pair_id):
|
||||
_directus("POST", "/items/db_pairs_db_question", token,
|
||||
{"db_pairs_id": pair_id, "db_question_id": qid})
|
||||
|
||||
# Neue Wörter hinzufügen
|
||||
words_add = body.get("words_add", [])
|
||||
# Statement-ID und Question-ID nochmal laden
|
||||
s_junc2, _ = _directus("GET", f"/items/db_pairs_db_statement?filter[db_pairs_id][_eq]={pair_id}&fields=db_statement_id&limit=1", token)
|
||||
stmt_id_edit = ((s_junc2.get("data") or [{}])[0] or {}).get("db_statement_id")
|
||||
q_junc2, _ = _directus("GET", f"/items/db_pairs_db_question?filter[db_pairs_id][_eq]={pair_id}&fields=db_question_id&limit=1", token)
|
||||
q_id_edit = ((q_junc2.get("data") or [{}])[0] or {}).get("db_question_id")
|
||||
|
||||
for we in words_add:
|
||||
titel_de = (we.get("titel_de") or "").strip()
|
||||
w_level = int(we.get("level") or 50)
|
||||
link_to = we.get("link_to", "both")
|
||||
if not titel_de:
|
||||
continue
|
||||
try:
|
||||
wid, _ = _find_or_create_db_word(titel_de, w_level, token)
|
||||
if link_to in ("statement", "both") and stmt_id_edit:
|
||||
_directus("POST", "/items/db_statement_db_words", token, {"db_statement_id": stmt_id_edit, "db_words_id": wid})
|
||||
if link_to in ("question", "both") and q_id_edit:
|
||||
_directus("POST", "/items/db_question_db_words", token, {"db_question_id": q_id_edit, "db_words_id": wid})
|
||||
except Exception as e:
|
||||
print(f"[db_pair_patch] word error '{titel_de}': {e}")
|
||||
|
||||
# Wörter entfernen
|
||||
words_remove = body.get("words_remove", [])
|
||||
for wr in words_remove:
|
||||
link_to = wr.get("link_to", "both")
|
||||
junction_id = wr.get("junction_id")
|
||||
if not junction_id:
|
||||
continue
|
||||
if link_to in ("statement", "both"):
|
||||
_directus("DELETE", f"/items/db_statement_db_words/{junction_id}", token)
|
||||
if link_to in ("question", "both"):
|
||||
_directus("DELETE", f"/items/db_question_db_words/{junction_id}", token)
|
||||
|
||||
return jsonify({"ok": True})
|
||||
|
||||
else: # DELETE
|
||||
|
||||
@@ -440,7 +440,7 @@ export async function createDbPair(
|
||||
question_de?: string
|
||||
statement_de: string
|
||||
level: number
|
||||
words: { titel_de: string; level: number }[]
|
||||
words: { titel_de: string; level: number; link_to: 'question' | 'statement' | 'both' }[]
|
||||
},
|
||||
token: string
|
||||
): Promise<{ ok: boolean; pair_id: string; statement_id: string; question_id: string | null }> {
|
||||
@@ -465,7 +465,13 @@ export async function getDbObjectWords(objectId: string, token: string): Promise
|
||||
|
||||
export async function updateDbPair(
|
||||
pairId: string,
|
||||
payload: { level?: number; question_de?: string; statement_de?: string },
|
||||
payload: {
|
||||
level?: number
|
||||
question_de?: string
|
||||
statement_de?: string
|
||||
words_add?: { titel_de: string; level: number; link_to: 'question' | 'statement' | 'both' }[]
|
||||
words_remove?: { junction_id: number; link_to: 'question' | 'statement' | 'both' }[]
|
||||
},
|
||||
token: string
|
||||
): Promise<void> {
|
||||
const res = await fetch(`/api/directus/db-pairs/${pairId}`, {
|
||||
|
||||
@@ -6,14 +6,14 @@ import {
|
||||
getDbPictures,
|
||||
getDbObjects,
|
||||
getDbObjectPairs,
|
||||
getDbObjectWords,
|
||||
getDbPictureWords,
|
||||
createDbPair,
|
||||
updateDbPair,
|
||||
deleteDbPair,
|
||||
directusAssetUrl,
|
||||
} from '../api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import type { DbPicture, DbObject, DbPair, CanvasObject } from '../types'
|
||||
import type { DbPicture, DbObject, DbPair, DbPairWordEntry, CanvasObject } from '../types'
|
||||
|
||||
const ChevronLeftIcon = () => (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
@@ -31,14 +31,13 @@ const ChevronRightIcon = () => (
|
||||
interface PendingWord {
|
||||
titel_de: string
|
||||
level: number
|
||||
link_to: 'question' | 'statement' | 'both'
|
||||
}
|
||||
|
||||
import type { DbWord } from '../types'
|
||||
|
||||
interface PairFormProps {
|
||||
objectId: string
|
||||
token: string
|
||||
suggestions: DbWord[]
|
||||
suggestions: { titel_de: string; level: number }[]
|
||||
onSaved: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
@@ -54,11 +53,11 @@ function PairForm({ objectId, token, suggestions, onSaved, onCancel }: PairFormP
|
||||
const [error, setError] = useState('')
|
||||
const wordInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const addWord = (titel_de?: string, lvl?: number) => {
|
||||
const addWord = (titel_de?: string, lvl?: number, defaultLinkTo: 'question' | 'statement' | 'both' = 'both') => {
|
||||
const t = (titel_de ?? wordInput).trim()
|
||||
const l = lvl ?? wordLevel
|
||||
if (!t || words.some(w => w.titel_de === t)) { if (!titel_de) setWordInput(''); return }
|
||||
setWords(prev => [...prev, { titel_de: t, level: l }])
|
||||
setWords(prev => [...prev, { titel_de: t, level: l, link_to: defaultLinkTo }])
|
||||
if (!titel_de) { setWordInput(''); setWordLevel(50); wordInputRef.current?.focus() }
|
||||
}
|
||||
|
||||
@@ -136,17 +135,17 @@ function PairForm({ objectId, token, suggestions, onSaved, onCancel }: PairFormP
|
||||
{/* Words input */}
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>
|
||||
Wörter (werden an Frage + Aussage verknüpft)
|
||||
Wörter
|
||||
</label>
|
||||
|
||||
{/* Vorschläge aus dem Objekt */}
|
||||
{/* Vorschläge aus dem Bild */}
|
||||
{suggestions.length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 6 }}>
|
||||
{suggestions.map(s => {
|
||||
{suggestions.map((s, idx) => {
|
||||
const already = words.some(w => w.titel_de === s.titel_de)
|
||||
return (
|
||||
<button
|
||||
key={s.word_id}
|
||||
key={idx}
|
||||
onClick={() => addWord(s.titel_de, s.level)}
|
||||
disabled={already}
|
||||
style={{
|
||||
@@ -196,11 +195,26 @@ function PairForm({ objectId, token, suggestions, onSaved, onCancel }: PairFormP
|
||||
{words.map((w, i) => (
|
||||
<span key={i} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
padding: '2px 6px', borderRadius: 'var(--r-full)',
|
||||
padding: '2px 4px 2px 7px', borderRadius: 'var(--r-full)',
|
||||
background: 'var(--primary-muted)', border: '1px solid color-mix(in srgb, var(--primary) 30%, transparent)',
|
||||
fontSize: 11, color: 'var(--primary)',
|
||||
}}>
|
||||
{w.titel_de} <span style={{ color: 'var(--text-2)', fontSize: 10 }}>L{w.level}</span>
|
||||
{w.titel_de}
|
||||
<span style={{ color: 'var(--text-2)', fontSize: 10 }}>L{w.level}</span>
|
||||
{/* Link-to Toggle */}
|
||||
{(['question', 'statement', 'both'] as const).map(lt => (
|
||||
<button
|
||||
key={lt}
|
||||
onClick={() => setWords(prev => prev.map((x, j) => j === i ? { ...x, link_to: lt } : x))}
|
||||
style={{
|
||||
padding: '0 3px', borderRadius: 2, fontSize: 9, border: '1px solid currentColor',
|
||||
cursor: 'pointer', lineHeight: 1.4,
|
||||
background: w.link_to === lt ? 'var(--primary)' : 'transparent',
|
||||
color: w.link_to === lt ? '#fff' : 'var(--primary)',
|
||||
}}
|
||||
title={lt === 'question' ? 'Nur Frage' : lt === 'statement' ? 'Nur Aussage' : 'Beide'}
|
||||
>{lt === 'question' ? 'F' : lt === 'statement' ? 'A' : 'B'}</button>
|
||||
))}
|
||||
<button
|
||||
onClick={() => setWords(prev => prev.filter((_, j) => j !== i))}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--primary)', padding: 0, lineHeight: 1, fontSize: 12 }}
|
||||
@@ -235,7 +249,7 @@ interface PairsListProps {
|
||||
loading: boolean
|
||||
objectId: string | null
|
||||
token: string
|
||||
suggestions: DbWord[]
|
||||
suggestions: { titel_de: string; level: number }[]
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
@@ -247,11 +261,40 @@ function PairsList({ pairs, loading, objectId, token, suggestions, onRefresh }:
|
||||
const [editStatement, setEditStatement] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
// Word-edit state
|
||||
const [editWords, setEditWords] = useState<(DbPairWordEntry & { link_to: 'question' | 'statement' | 'both' })[]>([])
|
||||
const [editWordInput, setEditWordInput] = useState('')
|
||||
const [editWordLevel, setEditWordLevel] = useState(50)
|
||||
const [editWordLinkTo, setEditWordLinkTo] = useState<'question' | 'statement' | 'both'>('both')
|
||||
const [wordsToRemove, setWordsToRemove] = useState<{ junction_id: number; link_to: 'question' | 'statement' | 'both' }[]>([])
|
||||
const [newWords, setNewWords] = useState<PendingWord[]>([])
|
||||
|
||||
const startEdit = (pair: DbPair) => {
|
||||
setEditingId(pair.id)
|
||||
setEditLevel(pair.level)
|
||||
setEditStatement(pair.statements[0]?.statement_de ?? '')
|
||||
setEditQuestion(pair.questions[0]?.question_de ?? '')
|
||||
// Wörter aus Question und Statement kombinieren (dedupliziert nach word_id)
|
||||
const qWords = (pair.questions[0]?.words ?? []).map(w => ({ ...w, link_to: 'question' as const }))
|
||||
const sWords = (pair.statements[0]?.words ?? []).map(w => ({ ...w, link_to: 'statement' as const }))
|
||||
// Kombiniere: wenn Wort in beiden → link_to = 'both'
|
||||
const combined: (DbPairWordEntry & { link_to: 'question' | 'statement' | 'both' })[] = []
|
||||
const seen = new Map<string, number>()
|
||||
for (const w of [...qWords, ...sWords]) {
|
||||
const idx = seen.get(w.word_id)
|
||||
if (idx !== undefined) {
|
||||
combined[idx] = { ...combined[idx], link_to: 'both' }
|
||||
} else {
|
||||
seen.set(w.word_id, combined.length)
|
||||
combined.push({ ...w })
|
||||
}
|
||||
}
|
||||
setEditWords(combined)
|
||||
setWordsToRemove([])
|
||||
setNewWords([])
|
||||
setEditWordInput('')
|
||||
setEditWordLevel(50)
|
||||
setEditWordLinkTo('both')
|
||||
}
|
||||
|
||||
const saveEdit = async () => {
|
||||
@@ -262,6 +305,8 @@ function PairsList({ pairs, loading, objectId, token, suggestions, onRefresh }:
|
||||
level: editLevel,
|
||||
statement_de: editStatement,
|
||||
question_de: editQuestion,
|
||||
words_add: newWords,
|
||||
words_remove: wordsToRemove,
|
||||
}, token)
|
||||
setEditingId(null)
|
||||
onRefresh()
|
||||
@@ -315,6 +360,93 @@ function PairsList({ pairs, loading, objectId, token, suggestions, onRefresh }:
|
||||
placeholder="Frage (optional)…"
|
||||
style={{ width: '100%', padding: '5px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 12, resize: 'vertical', boxSizing: 'border-box' }}
|
||||
/>
|
||||
|
||||
{/* Wörter im Edit-Modus */}
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: 'var(--text-2)', display: 'block', marginBottom: 4 }}>Wörter</label>
|
||||
{/* Vorhandene Wörter */}
|
||||
{editWords.length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 6 }}>
|
||||
{editWords.map((w) => (
|
||||
<span key={w.junction_id} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
padding: '2px 4px 2px 7px', borderRadius: 'var(--r-full)',
|
||||
background: wordsToRemove.some(r => r.junction_id === w.junction_id) ? 'var(--surface-2)' : 'var(--primary-muted)',
|
||||
border: '1px solid color-mix(in srgb, var(--primary) 30%, transparent)',
|
||||
fontSize: 11, color: wordsToRemove.some(r => r.junction_id === w.junction_id) ? 'var(--text-2)' : 'var(--primary)',
|
||||
textDecoration: wordsToRemove.some(r => r.junction_id === w.junction_id) ? 'line-through' : 'none',
|
||||
opacity: wordsToRemove.some(r => r.junction_id === w.junction_id) ? 0.5 : 1,
|
||||
}}>
|
||||
{w.titel_de} <span style={{ fontSize: 10, color: 'var(--text-2)' }}>L{w.level}</span>
|
||||
<span style={{ fontSize: 9, color: 'var(--text-2)', marginLeft: 2 }}>
|
||||
{w.link_to === 'question' ? 'F' : w.link_to === 'statement' ? 'A' : 'B'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (wordsToRemove.some(r => r.junction_id === w.junction_id)) {
|
||||
setWordsToRemove(prev => prev.filter(r => r.junction_id !== w.junction_id))
|
||||
} else {
|
||||
setWordsToRemove(prev => [...prev, { junction_id: w.junction_id, link_to: w.link_to }])
|
||||
}
|
||||
}}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'inherit', padding: 0, lineHeight: 1, fontSize: 12 }}
|
||||
title={wordsToRemove.some(r => r.junction_id === w.junction_id) ? 'Wiederherstellen' : 'Entfernen'}
|
||||
>{wordsToRemove.some(r => r.junction_id === w.junction_id) ? '↩' : '×'}</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Neues Wort hinzufügen */}
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<input
|
||||
value={editWordInput}
|
||||
onChange={e => setEditWordInput(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') {
|
||||
const t = editWordInput.trim()
|
||||
if (t) { setNewWords(prev => [...prev, { titel_de: t, level: editWordLevel, link_to: editWordLinkTo }]); setEditWordInput('') }
|
||||
}
|
||||
}}
|
||||
placeholder="Wort…"
|
||||
style={{ flex: 1, minWidth: 80, padding: '4px 7px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 12 }}
|
||||
/>
|
||||
<input
|
||||
type="number" min={1} max={100} value={editWordLevel}
|
||||
onChange={e => setEditWordLevel(Math.min(100, Math.max(1, Number(e.target.value))))}
|
||||
style={{ width: 44, padding: '4px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 12, textAlign: 'center' }}
|
||||
/>
|
||||
<select
|
||||
value={editWordLinkTo}
|
||||
onChange={e => setEditWordLinkTo(e.target.value as 'question' | 'statement' | 'both')}
|
||||
style={{ padding: '4px 4px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', color: 'var(--text-1)', fontSize: 11 }}
|
||||
>
|
||||
<option value="both">Beide</option>
|
||||
<option value="question">Frage</option>
|
||||
<option value="statement">Aussage</option>
|
||||
</select>
|
||||
<button className="btn-ghost btn-sm" onClick={() => {
|
||||
const t = editWordInput.trim()
|
||||
if (t) { setNewWords(prev => [...prev, { titel_de: t, level: editWordLevel, link_to: editWordLinkTo }]); setEditWordInput('') }
|
||||
}}>+</button>
|
||||
</div>
|
||||
{newWords.length > 0 && (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6 }}>
|
||||
{newWords.map((w, i) => (
|
||||
<span key={i} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
padding: '2px 4px 2px 7px', borderRadius: 'var(--r-full)',
|
||||
background: 'var(--surface-2)', border: '1px solid var(--border)',
|
||||
fontSize: 11, color: 'var(--text-1)',
|
||||
}}>
|
||||
{w.titel_de} <span style={{ fontSize: 10, color: 'var(--text-2)' }}>L{w.level} {w.link_to === 'question' ? 'F' : w.link_to === 'statement' ? 'A' : 'B'}</span>
|
||||
<button onClick={() => setNewWords(prev => prev.filter((_, j) => j !== i))}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: 0, fontSize: 12, lineHeight: 1 }}>×</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button className="btn-primary btn-sm" style={{ flex: 1 }} onClick={saveEdit} disabled={saving || !editStatement.trim()}>
|
||||
{saving ? 'Speichere…' : 'Speichern'}
|
||||
@@ -388,7 +520,7 @@ export default function GenerateIt() {
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const [pairs, setPairs] = useState<DbPair[]>([])
|
||||
const [pairsLoading, setPairsLoading] = useState(false)
|
||||
const [objectWords, setObjectWords] = useState<DbWord[]>([])
|
||||
const [pictureWords, setPictureWords] = useState<{ titel_de: string; level: number }[]>([])
|
||||
|
||||
const currentPicture: DbPicture | null =
|
||||
currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null
|
||||
@@ -423,17 +555,22 @@ export default function GenerateIt() {
|
||||
.catch(console.error)
|
||||
}, [currentPicture?.id, token])
|
||||
|
||||
// Load pairs + words when selected object changes
|
||||
// Load picture words when picture changes
|
||||
useEffect(() => {
|
||||
if (!selectedObjId || !token) { setPairs([]); setObjectWords([]); return }
|
||||
if (!currentPicture || !token) { setPictureWords([]); return }
|
||||
getDbPictureWords(currentPicture.id, token)
|
||||
.then(words => setPictureWords(words.map(w => ({ titel_de: w.titel_de, level: w.level }))))
|
||||
.catch(console.error)
|
||||
}, [currentPicture?.id, token])
|
||||
|
||||
// Load pairs when selected object changes
|
||||
useEffect(() => {
|
||||
if (!selectedObjId || !token) { setPairs([]); return }
|
||||
setPairsLoading(true)
|
||||
getDbObjectPairs(selectedObjId, token)
|
||||
.then(setPairs)
|
||||
.catch(console.error)
|
||||
.finally(() => setPairsLoading(false))
|
||||
getDbObjectWords(selectedObjId, token)
|
||||
.then(setObjectWords)
|
||||
.catch(console.error)
|
||||
}, [selectedObjId, token])
|
||||
|
||||
const refreshPairs = () => {
|
||||
@@ -506,16 +643,16 @@ export default function GenerateIt() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Wörter des gewählten Objekts */}
|
||||
{objectWords.length > 0 && (
|
||||
{/* Wörter des aktuellen Bildes */}
|
||||
{pictureWords.length > 0 && (
|
||||
<div className="sidebar-panel">
|
||||
<h3 className="sidebar-heading">
|
||||
Wörter
|
||||
<span className="badge">{objectWords.length}</span>
|
||||
Bild-Wörter
|
||||
<span className="badge">{pictureWords.length}</span>
|
||||
</h3>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4 }}>
|
||||
{objectWords.map(w => (
|
||||
<span key={w.word_id} style={{
|
||||
{pictureWords.map((w, i) => (
|
||||
<span key={i} style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 3,
|
||||
padding: '2px 7px', borderRadius: 'var(--r-full)',
|
||||
background: 'var(--surface-2)', border: '1px solid var(--border)',
|
||||
@@ -575,7 +712,7 @@ export default function GenerateIt() {
|
||||
loading={pairsLoading}
|
||||
objectId={selectedObjId}
|
||||
token={token ?? ''}
|
||||
suggestions={objectWords}
|
||||
suggestions={pictureWords}
|
||||
onRefresh={refreshPairs}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -80,11 +80,19 @@ export interface DbWord {
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface DbPairWordEntry {
|
||||
junction_id: number
|
||||
word_id: string
|
||||
titel_de: string
|
||||
level: number
|
||||
}
|
||||
|
||||
export interface DbPairQuestion {
|
||||
id: string
|
||||
question_de: string
|
||||
level: number
|
||||
status: string
|
||||
words: DbPairWordEntry[]
|
||||
}
|
||||
|
||||
export interface DbPairStatement {
|
||||
@@ -92,6 +100,7 @@ export interface DbPairStatement {
|
||||
statement_de: string
|
||||
level: number
|
||||
status: string
|
||||
words: DbPairWordEntry[]
|
||||
}
|
||||
|
||||
export interface DbPair {
|
||||
|
||||
Reference in New Issue
Block a user