From 91e8b92bdcaf34d6f3c2339b4211f69426e047f1 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 29 Apr 2026 10:56:36 +0200 Subject: [PATCH] =?UTF-8?q?Add=20topic=20suggestions:=20search=20"banana"?= =?UTF-8?q?=20=E2=86=92=20suggests=20"Fruit/Obst"=20topic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 31 curated topics with 683 words (Fruit, Animals, Body, Eye, Sports, School, Family, Weather, etc.). When user types a word that belongs to a topic, the topic appears as a suggestion with "Alle laden" button. Clicking "Alle laden" fetches all words from that topic via Kaikki and displays them for easy selection into a learning unit. New endpoint: GET /api/vocabulary/topics?q=banana New table: vocabulary_topics (topic, words[], word_count) Co-Authored-By: Claude Opus 4.6 (1M context) --- backend-lehrer/vocabulary/api.py | 26 +++++++++++++++ studio-v2/app/vocabulary/page.tsx | 55 ++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/backend-lehrer/vocabulary/api.py b/backend-lehrer/vocabulary/api.py index 1b670a5..5915784 100644 --- a/backend-lehrer/vocabulary/api.py +++ b/backend-lehrer/vocabulary/api.py @@ -431,6 +431,32 @@ async def api_enrich_images(word_ids: List[str] = None): return {"enriched": count, "total": len(word_ids)} +@router.get("/topics") +async def api_get_topics(q: str = Query("", description="Search topic or word")): + """Find topics matching a search word. Returns related word lists. + + If q matches a topic name → returns that topic. + If q matches a word in any topic → returns all topics containing that word. + """ + from vocabulary.db import get_pool + pool = await get_pool() + async with pool.acquire() as conn: + if not q.strip(): + rows = await conn.fetch("SELECT topic, words, word_count FROM vocabulary_topics ORDER BY topic LIMIT 50") + else: + q_lower = q.strip().lower() + rows = await conn.fetch(""" + SELECT topic, words, word_count FROM vocabulary_topics + WHERE lower(topic) LIKE $1 OR $2 = ANY(words) + ORDER BY word_count DESC + """, f"%{q_lower}%", q_lower) + + return { + "topics": [{"topic": r["topic"], "words": list(r["words"]), "word_count": r["word_count"]} for r in rows], + "query": q, + } + + class TranslateRequest(BaseModel): word_ids: List[str] target_language: str diff --git a/studio-v2/app/vocabulary/page.tsx b/studio-v2/app/vocabulary/page.tsx index dd0600f..b949e0a 100644 --- a/studio-v2/app/vocabulary/page.tsx +++ b/studio-v2/app/vocabulary/page.tsx @@ -38,6 +38,8 @@ export default function VocabularyPage() { const [posFilter, setPosFilter] = useState('') const [diffFilter, setDiffFilter] = useState(0) const [searchLang, setSearchLang] = useState('en') + const [topics, setTopics] = useState<{ topic: string; words: string[]; word_count: number }[]>([]) + const [showTopics, setShowTopics] = useState(false) // Unit builder const [selectedWords, setSelectedWords] = useState([]) @@ -84,6 +86,15 @@ export default function VocabularyPage() { const data = await resp.json() setResults(data.words || []) } + + // Also search for matching topics + if (query.trim()) { + const topicResp = await fetch(`${getApiBase()}/api/vocabulary/topics?q=${encodeURIComponent(query)}`) + if (topicResp.ok) { + const topicData = await topicResp.json() + setTopics(topicData.topics || []) + } + } } catch (err) { console.error('Search failed:', err) } finally { @@ -218,7 +229,49 @@ export default function VocabularyPage() { )} - {!isSearching && results.length === 0 && query.trim() && ( + {/* Topic suggestions */} + {topics.length > 0 && ( +
+ {topics.map(topic => ( +
+
+ + 💡 {topic.topic} ({topic.word_count}) + + +
+
+ {topic.words.slice(0, 15).map(w => ( + {w} + ))} + {topic.words.length > 15 && ( + +{topic.words.length - 15} + )} +
+
+ ))} +
+ )} + + {!isSearching && results.length === 0 && query.trim() && topics.length === 0 && (

Keine Ergebnisse fuer "{query}"