feat: read-only drawer with scene, activity, edges, memories
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
"""Read-only chat drawer (T24).
|
||||
|
||||
Returns an HTML partial rendered into the chat shell's `<aside id="drawer">`
|
||||
on first reveal. Shows the current scene + container, per-entity activity,
|
||||
host <-> you edges, pinned memories with an `n / cap` counter, and recent
|
||||
witnessed memories from the host's POV with significance markers.
|
||||
|
||||
Edit affordances are added in T25; this endpoint is intentionally read-only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from chat.state.edges import get_edge
|
||||
from chat.state.entities import get_bot, get_you
|
||||
from chat.state.memory import get_pinned
|
||||
from chat.state.world import active_scene, get_activity, get_chat, get_container
|
||||
from chat.web.bots import get_conn
|
||||
|
||||
TEMPLATES = Jinja2Templates(
|
||||
directory=str(Path(__file__).resolve().parent.parent / "templates")
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Soft cap on pinned memories per owner (§8.5). Surfaced in the drawer header
|
||||
# as `pinned|length / pin_cap`; eviction logic itself lives in T22.
|
||||
PIN_CAP = 8
|
||||
|
||||
# Recent-memories list is bounded to keep the drawer cheap to render.
|
||||
RECENT_LIMIT = 10
|
||||
|
||||
|
||||
@router.get("/chats/{chat_id}/drawer", response_class=HTMLResponse)
|
||||
async def drawer(chat_id: str, request: Request, conn=Depends(get_conn)):
|
||||
chat = get_chat(conn, chat_id)
|
||||
if chat is None:
|
||||
raise HTTPException(status_code=404, detail=f"chat not found: {chat_id}")
|
||||
|
||||
host_bot = get_bot(conn, chat["host_bot_id"])
|
||||
if host_bot is None:
|
||||
raise HTTPException(
|
||||
status_code=404, detail=f"host bot not found: {chat['host_bot_id']}"
|
||||
)
|
||||
you_entity = get_you(conn) or {"name": "you", "pronouns": "", "persona": ""}
|
||||
|
||||
scene = active_scene(conn, chat_id)
|
||||
container = None
|
||||
if scene and scene.get("container_id") is not None:
|
||||
container = get_container(conn, scene["container_id"])
|
||||
|
||||
you_activity = get_activity(conn, "you")
|
||||
bot_activity = get_activity(conn, chat["host_bot_id"])
|
||||
|
||||
edge_b2y = get_edge(conn, chat["host_bot_id"], "you")
|
||||
edge_y2b = get_edge(conn, "you", chat["host_bot_id"])
|
||||
|
||||
# Recent memories from host's POV (witness_host = 1), most recent first.
|
||||
# Raw query keeps this read self-contained — no projector helper exposes
|
||||
# "latest N for an owner" yet and the drawer is the only consumer.
|
||||
recent_rows = conn.execute(
|
||||
"""
|
||||
SELECT id, pov_summary, significance, pinned, created_at
|
||||
FROM memories
|
||||
WHERE owner_id = ? AND witness_host = 1
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(chat["host_bot_id"], RECENT_LIMIT),
|
||||
).fetchall()
|
||||
recent_memories = [
|
||||
{
|
||||
"id": r[0],
|
||||
"pov_summary": r[1],
|
||||
"significance": r[2],
|
||||
"pinned": r[3],
|
||||
"created_at": r[4],
|
||||
}
|
||||
for r in recent_rows
|
||||
]
|
||||
|
||||
pinned = get_pinned(conn, chat["host_bot_id"])
|
||||
|
||||
return TEMPLATES.TemplateResponse(
|
||||
request,
|
||||
"_drawer.html",
|
||||
{
|
||||
"chat": chat,
|
||||
"host_bot": host_bot,
|
||||
"you_entity": you_entity,
|
||||
"scene": scene,
|
||||
"container": container,
|
||||
"you_activity": you_activity,
|
||||
"bot_activity": bot_activity,
|
||||
"edge_b2y": edge_b2y,
|
||||
"edge_y2b": edge_y2b,
|
||||
"recent_memories": recent_memories,
|
||||
"pinned": pinned,
|
||||
"pin_cap": PIN_CAP,
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user