diff --git a/backend/database.py b/backend/database.py index c56a2f6..cff0f89 100644 --- a/backend/database.py +++ b/backend/database.py @@ -2075,6 +2075,18 @@ def _migrate(conn_factory): _seed_help_articles(conn) logger.info("Migration: Hilfe/FAQ-Tabelle bereit.") + conn.executescript(""" + CREATE TABLE IF NOT EXISTS bday_ki_cache ( + dog_id INTEGER NOT NULL, + year INTEGER NOT NULL, + mode TEXT NOT NULL, -- 'tomorrow' | 'today' + content TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + PRIMARY KEY (dog_id, year, mode) + ); + """) + logger.info("Migration: bday_ki_cache Tabelle bereit.") + # ---- Feature: Subscription-Tier ---- try: conn.execute("ALTER TABLE users ADD COLUMN subscription_tier TEXT DEFAULT 'standard'") diff --git a/backend/main.py b/backend/main.py index 78ab952..598438a 100644 --- a/backend/main.py +++ b/backend/main.py @@ -327,7 +327,7 @@ MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media") os.makedirs(MEDIA_DIR, exist_ok=True) app.mount("/media", StaticFiles(directory=MEDIA_DIR), name="media") -APP_VER = "779" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "780" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/ki.py b/backend/routes/ki.py index 80d663c..f43f87e 100644 --- a/backend/routes/ki.py +++ b/backend/routes/ki.py @@ -167,6 +167,85 @@ def _log_rasse_request(user_id: int): ) +# ------------------------------------------------------------------ +# POST /ki/geburtstag — Geburtstags-Überraschungsideen (kostenlos für alle) +# ------------------------------------------------------------------ + +class BirthdayRequest(BaseModel): + dog_id: int + name: str + rasse: Optional[str] = None + alter: Optional[int] = None + mode: str = "tomorrow" # "tomorrow" | "today" + +@router.post("/geburtstag") +async def ki_geburtstag(req: BirthdayRequest, request: Request, + user=Depends(get_current_user)): + """Kostenlose KI-Geburtstagsideen — kein Premium nötig, 1x/Tag, DB-gecacht.""" + from datetime import date + year = date.today().year + mode = req.mode if req.mode in ("tomorrow", "today") else "tomorrow" + + # Aus DB-Cache zurückgeben wenn bereits generiert + with db() as conn: + cached = conn.execute( + "SELECT content FROM bday_ki_cache WHERE dog_id=? AND year=? AND mode=?", + (req.dog_id, year, mode) + ).fetchone() + if cached: + return {"answer": cached["content"], "cached": True} + + # Rate-Limit: 1 Generierung pro Tag pro User + rl_check(request, max_requests=1, window_seconds=86400, key=f"ki_bday_{req.dog_id}_{mode}") + + name = req.name.strip()[:40] or "deinen Hund" + rasse = req.rasse or None + alter = req.alter + rasse_str = f"({rasse})" if rasse else "" + + if mode == "today": + # Aus Sicht des Hundes — was er sich für seinen Geburtstag vorstellt + alter_str = f"{alter}. Geburtstag" if alter else "Geburtstag" + system = ( + "Du bist ein Hund und erzählst aus deiner eigenen Perspektive. " + "Schreibe auf Deutsch, verspielt, liebevoll und mit Hundelogik. " + "Verwende typische Hundegedanken: Fressen, Gassi, Schmusen, Spielen, Gerüche." + ) + prompt = ( + f"Ich bin {name} {rasse_str} und heute ist mein {alter_str}! " + f"Erzähl in meiner Stimme (als Hund), wie ich mir den perfekten Geburtstagstag vorgestellt habe — " + f"von Morgen bis Abend. Was möchte ich erleben, fressen, riechen, spielen? " + f"Ca. 150 Wörter, herzlich und humorvoll." + ) + else: + # Überraschungsideen für morgen + alter_str = f"{alter}. Geburtstag" if alter else "Geburtstag" + system = ( + "Du bist ein begeisterter Hundefreund mit vielen kreativen Ideen. " + "Antworte auf Deutsch, herzlich, konkret und mit einer Prise Humor. " + "Fokus auf praktische, umsetzbare Überraschungen." + ) + prompt = ( + f"Morgen ist der {alter_str} von {name} {rasse_str}! " + f"Was können wir {name} besonders gönnen? " + f"Gib 5 konkrete, liebevolle Überraschungsideen — von einfach bis aufwendig, " + f"jeweils mit einem Satz warum Hunde das lieben." + ) + + try: + answer = await ki_module.complete( + system=system, prompt=prompt, max_tokens=600, requires_premium=False, + ) + with db() as conn: + conn.execute( + "INSERT OR REPLACE INTO bday_ki_cache (dog_id, year, mode, content) VALUES (?,?,?,?)", + (req.dog_id, year, mode, answer) + ) + return {"answer": answer, "cached": False} + except ki_module.KIUnavailableError: + raise HTTPException(503, "KI momentan nicht verfügbar.") + + # ------------------------------------------------------------------ # POST /ki/rasse-erkennung — Vision-basierte Rassenerkennung # ------------------------------------------------------------------ diff --git a/backend/static/index.html b/backend/static/index.html index bade895..66698b5 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -575,10 +575,10 @@
- - - - + + + + diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 8131af6..2c57217 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '779'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '780'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.5.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/worlds.js b/backend/static/js/worlds.js index 6daaaf4..71018a3 100644 --- a/backend/static/js/worlds.js +++ b/backend/static/js/worlds.js @@ -1322,8 +1322,8 @@ window.Worlds = (() => { .bday-fw2 { display:inline-block; animation: bday-fw2 1.1s ease-in-out infinite .2s; } .bday-fw3 { display:inline-block; animation: bday-fw3 1.6s ease-in-out infinite .4s; } -