From 2e6cf094cbb4ddcf72cd64092638b793b2321c49 Mon Sep 17 00:00:00 2001 From: admin Date: Sun, 10 May 2026 12:07:42 +0200 Subject: [PATCH] feat: object words in left sidebar + suggestions in pair form - Backend: GET /api/directus/db-objects//words via db_objects_db_words - GenerateIt: load objectWords on object select, show as chips in left sidebar - PairForm: show object words as clickable suggestion chips above word input (click to add, greyed out if already added) Co-Authored-By: Claude Sonnet 4.6 --- app.py | 27 ++++++++++ frontend/src/api.ts | 9 ++++ frontend/src/pages/GenerateIt.tsx | 87 ++++++++++++++++++++++++++----- 3 files changed, 110 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index 347d6e8..73c6f58 100644 --- a/app.py +++ b/app.py @@ -1877,6 +1877,33 @@ def directus_db_object_pairs(obj_id): return jsonify({"ok": True, "pair_id": pair_id, "statement_id": stmt_id, "question_id": q_id}) +@app.route("/api/directus/db-objects//words", methods=["GET"]) +def directus_db_object_words(obj_id): + """Gibt alle db_words zurück, die via db_objects_db_words mit dem Objekt verknüpft sind.""" + token = request.headers.get("Authorization", "") + data, s = _directus( + "GET", + f"/items/db_objects_db_words?filter[db_objects_id][_eq]={obj_id}" + f"&fields=id,db_words_id.id,db_words_id.titel_de,db_words_id.level,db_words_id.status&limit=500", + token, + ) + if s != 200: + return jsonify({"data": []}) + items = [] + for entry in (data.get("data") or []): + word = entry.get("db_words_id") or {} + if not isinstance(word, dict) or not word.get("id"): + continue + if word.get("status") == "archived": + continue + items.append({ + "word_id": word["id"], + "titel_de": word.get("titel_de", ""), + "level": word.get("level") or 50, + }) + return jsonify({"data": items}) + + @app.route("/api/directus/db-pairs/", methods=["PATCH", "DELETE"]) def directus_db_pair(pair_id): """PATCH: level + question/statement inline aktualisieren. diff --git a/frontend/src/api.ts b/frontend/src/api.ts index 89e1a4b..846f9e2 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -454,6 +454,15 @@ export async function createDbPair( return data } +export async function getDbObjectWords(objectId: string, token: string): Promise { + const res = await fetch(`/api/directus/db-objects/${objectId}/words`, { + headers: { Authorization: `Bearer ${token}` }, + }) + const data = await res.json() + if (!res.ok) throw new Error('Fehler beim Laden der Objekt-Wörter') + return data.data as DbWord[] +} + export async function updateDbPair( pairId: string, payload: { level?: number; question_de?: string; statement_de?: string }, diff --git a/frontend/src/pages/GenerateIt.tsx b/frontend/src/pages/GenerateIt.tsx index a7cd0ff..00196fc 100644 --- a/frontend/src/pages/GenerateIt.tsx +++ b/frontend/src/pages/GenerateIt.tsx @@ -6,6 +6,7 @@ import { getDbPictures, getDbObjects, getDbObjectPairs, + getDbObjectWords, createDbPair, updateDbPair, deleteDbPair, @@ -32,14 +33,17 @@ interface PendingWord { level: number } +import type { DbWord } from '../types' + interface PairFormProps { objectId: string token: string + suggestions: DbWord[] onSaved: () => void onCancel: () => void } -function PairForm({ objectId, token, onSaved, onCancel }: PairFormProps) { +function PairForm({ objectId, token, suggestions, onSaved, onCancel }: PairFormProps) { const [level, setLevel] = useState(50) const [questionDe, setQuestionDe] = useState('') const [statementDe, setStatementDe] = useState('') @@ -50,13 +54,12 @@ function PairForm({ objectId, token, onSaved, onCancel }: PairFormProps) { const [error, setError] = useState('') const wordInputRef = useRef(null) - const addWord = () => { - const t = wordInput.trim() - if (!t || words.some(w => w.titel_de === t)) { setWordInput(''); return } - setWords(prev => [...prev, { titel_de: t, level: wordLevel }]) - setWordInput('') - setWordLevel(50) - wordInputRef.current?.focus() + const addWord = (titel_de?: string, lvl?: number) => { + 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 }]) + if (!titel_de) { setWordInput(''); setWordLevel(50); wordInputRef.current?.focus() } } const handleSave = async () => { @@ -135,13 +138,41 @@ function PairForm({ objectId, token, onSaved, onCancel }: PairFormProps) { + + {/* Vorschläge aus dem Objekt */} + {suggestions.length > 0 && ( +
+ {suggestions.map(s => { + const already = words.some(w => w.titel_de === s.titel_de) + return ( + + ) + })} +
+ )} +
setWordInput(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addWord() }} - placeholder="titel_de…" + placeholder="Wort…" style={{ flex: 1, padding: '4px 8px', borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface-2)', @@ -157,7 +188,7 @@ function PairForm({ objectId, token, onSaved, onCancel }: PairFormProps) { color: 'var(--text-1)', fontFamily: 'var(--font)', fontSize: 12, textAlign: 'center', }} /> - +
{/* Word chips */} {words.length > 0 && ( @@ -204,10 +235,11 @@ interface PairsListProps { loading: boolean objectId: string | null token: string + suggestions: DbWord[] onRefresh: () => void } -function PairsList({ pairs, loading, objectId, token, onRefresh }: PairsListProps) { +function PairsList({ pairs, loading, objectId, token, suggestions, onRefresh }: PairsListProps) { const [showForm, setShowForm] = useState(false) const [editingId, setEditingId] = useState(null) const [editLevel, setEditLevel] = useState(50) @@ -333,6 +365,7 @@ function PairsList({ pairs, loading, objectId, token, onRefresh }: PairsListProp setShowForm(false)} /> @@ -355,6 +388,7 @@ export default function GenerateIt() { const [imageLoaded, setImageLoaded] = useState(false) const [pairs, setPairs] = useState([]) const [pairsLoading, setPairsLoading] = useState(false) + const [objectWords, setObjectWords] = useState([]) const currentPicture: DbPicture | null = currentIndex >= 0 && currentIndex < pictureList.length ? pictureList[currentIndex] : null @@ -389,14 +423,17 @@ export default function GenerateIt() { .catch(console.error) }, [currentPicture?.id, token]) - // Load pairs when selected object changes + // Load pairs + words when selected object changes useEffect(() => { - if (!selectedObjId || !token) { setPairs([]); return } + if (!selectedObjId || !token) { setPairs([]); setObjectWords([]); 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 = () => { @@ -468,6 +505,29 @@ export default function GenerateIt() { )} + + {/* Wörter des gewählten Objekts */} + {objectWords.length > 0 && ( +
+

+ Wörter + {objectWords.length} +

+
+ {objectWords.map(w => ( + + {w.titel_de} + L{w.level} + + ))} +
+
+ )} {/* Center: Canvas */} @@ -515,6 +575,7 @@ export default function GenerateIt() { loading={pairsLoading} objectId={selectedObjId} token={token ?? ''} + suggestions={objectWords} onRefresh={refreshPairs} />