"""Jinja environment and the context every template receives.""" from __future__ import annotations from pathlib import Path from typing import Any from fastapi import Request from fastapi.templating import Jinja2Templates from lembas import __version__ from lembas.config import settings from lembas.db.models import User from lembas.services.reasoning import format_duration TEMPLATE_DIR = Path(__file__).parent / "templates" STATIC_DIR = Path(__file__).parent / "static" templates = Jinja2Templates(directory=str(TEMPLATE_DIR)) templates.env.trim_blocks = True templates.env.lstrip_blocks = True # {{ message.reasoning_ms | duration }} -> "8 seconds" templates.env.filters["duration"] = format_duration def stable_hue(value: str) -> int: """A deterministic 0-359 hue for a string. Used for generated model avatars so each model gets its own colour without anyone choosing one, and the same model looks the same on every page and after every restart. Python's hash() is salted per process, hence md5. """ import hashlib digest = hashlib.md5(value.encode("utf-8"), usedforsecurity=False).digest() return int.from_bytes(digest[:2], "big") % 360 templates.env.filters["stable_hue"] = stable_hue def resolve_theme(user: User | None) -> str: """Theme to render with on the server. Only ever a first guess: the inline script in base.html corrects it from localStorage before first paint. Getting it close server-side is what stops a signed-in user seeing a flash of the wrong theme on every navigation. """ if user is not None: chosen = (user.settings_json or {}).get("theme") if chosen in ("moria", "shire"): return chosen return settings.default_theme def render( request: Request, template: str, context: dict[str, Any] | None = None, **kwargs: Any, ): """Render a template with the globals every page expects. Using this instead of templates.TemplateResponse directly is what guarantees `user` and `theme` are always defined, so templates never need to guard against a missing variable. """ user = getattr(request.state, "user", None) payload: dict[str, Any] = { "request": request, "user": user, "theme": resolve_theme(user), "version": __version__, "allow_signup": settings.allow_signup, } payload.update(context or {}) return templates.TemplateResponse(request, template, payload, **kwargs)