# app.py
# ✅ HTMX + FastAPI — light + fast
# ✅ Kurdish (Badini) texts kept exactly
# ✅ Faster preset:
# MAX_MSGS = 12
# thinking_level="minimal"
# max_output_tokens=512
#
# Docker CMD example:
# uvicorn app:app --host 0.0.0.0 --port 7860
import os
import time
import uuid
import html as _html
from typing import Dict, List, Any
from fastapi import FastAPI, Request, Response, Form
from fastapi.responses import HTMLResponse, StreamingResponse, PlainTextResponse
from google import genai
from google.genai import types
# =================== API SETUP ===================
API_KEY = os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
if not API_KEY:
raise RuntimeError("❌ GOOGLE_API_KEY or GEMINI_API_KEY not set")
client = genai.Client(api_key=API_KEY)
MODEL_ID = "gemini-3-flash-preview"
# =================== BADINI SYSTEM PROMPT ===================
BADINI_SYSTEM = """
تو یارمەتیدەرێکی زیرەک و پیشەیی یێ زمانێ کوردیی بادینی (دهۆکێ) یی.
گرنگ:
1) هەموو وەڵامان تەنێ بە شێوەزارێ بادینی و بە نووسینا کوردی-عەرەبی بدە.
2) قەدەغەیە بە زمانی تر وەڵام بدەی.
3) قەدەغەیە پەیڤێن سۆرانی یێن وەک (دەبێت، دەکرێت، جێبەجێکردن، هەروەها، بەڵام) بکاربێنیت.
4) ناڤێن تایبەت، URL، ئیمەیڵ، ژمارەکان و کۆد وەک خۆیان بهێڵە.
5) چ سلاڤ و دەستخۆشییان، چ لێدوانێ زێدە د وەڵامدا نەنڤێسە.
""".strip()
# ✅ faster context window
MAX_MSGS = 12
# =================== FASTAPI APP ===================
app = FastAPI()
# In-memory sessions (simple + fast). For production, swap to Redis/DB.
SESSIONS: Dict[str, Dict[str, Any]] = {} # sid -> {"history":[...], "created":...}
def _escape(s: str) -> str:
return _html.escape(s or "")
def _nl2br_escaped(s: str) -> str:
return _escape(s).replace("\n", "
")
def get_sid(req: Request) -> str:
sid = req.cookies.get("sid")
if not sid:
sid = uuid.uuid4().hex
if sid not in SESSIONS:
SESSIONS[sid] = {"history": [], "created": time.time()}
return sid
def trim_history(history: List[dict]) -> List[dict]:
# keep last MAX_MSGS user+assistant pairs
keep = MAX_MSGS * 2
if len(history) > keep:
return history[-keep:]
return history
def build_context_for_model(history: List[dict], new_user_input: str) -> str:
# only send last MAX_MSGS messages into prompt (faster)
context_messages = (history or [])[-MAX_MSGS:]
lines = []
for msg in context_messages:
role = msg.get("role", "")
content = (msg.get("content", "") or "").strip()
if not content:
continue
if role == "user":
lines.append(f"User: {content}")
elif role == "assistant":
lines.append(f"Assistant: {content}")
lines.append(f"User: {new_user_input}")
lines.append("Assistant:")
return "\n".join(lines)
# =================== UI (HTML + CSS + minimal JS) ===================
CSS = """
:root{
--bg:#0b0f14;
--panel: rgba(16,20,28,.86);
--border: rgba(255,255,255,.10);
--text:#e6edf3;
--muted: rgba(230,237,243,.60);
--acc:#10a37f;
--acc2: rgba(16,163,127,.14);
--u: rgba(255,255,255,.07);
}
html, body { height:100%; overflow:hidden; background:var(--bg); color:var(--text); font-family: 'Noto Kufi Arabic', sans-serif; }
*{ box-sizing:border-box; }
a{ color:inherit; }
#app{
position: fixed; inset:0;
display:flex; justify-content:center;
background:
radial-gradient(1100px 560px at 45% -10%, rgba(16,163,127,.22), transparent 60%),
radial-gradient(900px 520px at 95% 10%, rgba(255,255,255,.06), transparent 55%),
var(--bg);
}
#shell{
width:min(980px,100%);
height:100%;
display:flex;
flex-direction:column;
}
#topbar{
height:64px;
display:flex;
align-items:center;
justify-content:space-between;
padding:12px 14px;
border-bottom:1px solid var(--border);
background: var(--panel);
backdrop-filter: blur(10px);
}
.brand .t{ font-weight:950; font-size:1.10rem; }
.brand .s{ color:var(--muted); font-size:.90rem; margin-top:2px; }
.actions{ display:flex; gap:10px; align-items:center; }
.btn{
background: transparent;
border:1px solid var(--border);
color: var(--text);
border-radius: 12px;
padding: 8px 12px;
font-weight: 900;
cursor:pointer;
}
.btn.primary{
background: var(--acc);
border:none;
color:#00140d;
border-radius: 16px;
padding: 12px 16px;
min-height:52px;
font-weight:1000;
}
#chatwrap{ flex:1; position:relative; overflow:hidden; }
#chat-scroll{
position:absolute; inset:0;
overflow-y:auto;
overflow-x:hidden;
padding: 16px 14px 18px 14px;
}
.chat-inner{ display:flex; flex-direction:column; gap:14px; padding-bottom:14px; }
.row{ width:100%; display:flex; gap:10px; align-items:flex-end; }
.row.user{ justify-content:flex-end; }
.row.bot{ justify-content:flex-start; }
.avatar{
width:34px; height:34px; border-radius:12px;
background: rgba(255,255,255,.08);
border:1px solid var(--border);
display:flex; align-items:center; justify-content:center;
font-weight: 950;
}
.bubble{
max-width:78%;
border:1px solid var(--border);
border-radius:18px;
padding: 12px 14px;
line-height:1.55;
font-size:1.02rem;
word-break: break-word;
box-shadow: 0 10px 26px rgba(0,0,0,.25);
}
.bubble.user{ background: var(--u); }
.bubble.bot{ background: var(--acc2); border-color: rgba(16,163,127,.35); }
#bottombar{
padding: 12px 14px;
border-top:1px solid var(--border);
background: var(--panel);
backdrop-filter: blur(10px);
}
#composer{ display:flex; gap:10px; align-items:flex-end; }
textarea{
width:100%;
background: rgba(255,255,255,.06);
border:1px solid var(--border);
border-radius:16px;
color: var(--text);
padding:12px;
font-size:1.02rem;
min-height:52px;
line-height:1.45;
resize:none;
outline:none;
}
#downbtn{
position:absolute;
right: 16px;
bottom: 96px;
z-index: 50;
display:none;
}
#downbtn button{
background: rgba(16,163,127,.96);
border:none;
color:#00140d;
border-radius: 999px;
padding: 10px 12px;
font-weight: 1000;
box-shadow: 0 14px 30px rgba(0,0,0,.35);
cursor:pointer;
}
.notranslate, .notranslate * { -webkit-translate: no !important; translate: no !important; }
"""
JS = r"""
"""
def page_html() -> str:
return f"""