Admin: Word Search theme authoring + tidy word-pool chips

* New "Word Search themes" panel in the Games tab: enter a theme name + words,
  with live validation (4–8 letters, alpha, deduped) and a count vs the 28 needed
  to fill all three sizes. An " Suggest a word" button asks the LLM for one
  fresh word that fits the theme. Save/edit/remove; authored themes join the daily
  fallback rotation alongside the curated ones (wordsearch_themes table). The
  system still handles word distribution across sizes + placement.
* Daily Word pool's added-word chips now scroll within a bounded area so the
  console stays tidy as the list grows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-11 13:36:07 -04:00
parent 61f575ba6d
commit f71e760847
5 changed files with 268 additions and 2 deletions
+48
View File
@@ -350,6 +350,17 @@ class ClientErrorBody(BaseModel):
version: str = ""
class WordsearchThemeBody(BaseModel):
theme: str
words: list[str] = []
id: int | None = None
class WordsearchSuggestBody(BaseModel):
theme: str
existing: list[str] = []
class EmailStartRequest(BaseModel):
email: str
@@ -1512,6 +1523,43 @@ def create_app() -> FastAPI:
games.remove_pool_word(conn, word)
return games.pool_summary(conn)
# --- Admin: Word Search theme authoring ---
@app.get("/api/admin/wordsearch/themes")
def admin_ws_themes(request: Request) -> list[dict]:
with get_conn() as conn:
_require_admin(conn, request)
return games.list_wordsearch_themes(conn)
@app.post("/api/admin/wordsearch/themes")
def admin_ws_theme_save(body: WordsearchThemeBody, request: Request) -> dict:
with get_conn() as conn:
_require_admin(conn, request)
res = games.save_wordsearch_theme(conn, body.theme, body.words, body.id)
if "error" in res:
raise HTTPException(status_code=400, detail=res["error"])
return {**res, "themes": games.list_wordsearch_themes(conn)}
@app.delete("/api/admin/wordsearch/themes/{tid}")
def admin_ws_theme_remove(tid: int, request: Request) -> list[dict]:
with get_conn() as conn:
_require_admin(conn, request)
games.remove_wordsearch_theme(conn, tid)
return games.list_wordsearch_themes(conn)
@app.post("/api/admin/wordsearch/suggest")
def admin_ws_suggest(body: WordsearchSuggestBody, request: Request) -> dict:
with get_conn() as conn:
_require_admin(conn, request)
from .llm import LocalModelClient
try:
client = LocalModelClient.from_env()
except Exception: # noqa: BLE001
client = None
res = games.suggest_wordsearch_word(client, body.theme, body.existing)
if "error" in res:
raise HTTPException(status_code=503, detail=res["error"])
return res
@app.get("/api/since", response_model=FeedResponse)
def feed_since(ts: str = Query(...), prefs: str | None = Query(None)) -> FeedResponse:
# A calm welcome-back cue: accepted/non-dup/visible articles discovered