from __future__ import annotations import asyncio import uuid from fastapi import APIRouter, Depends, Request from fastapi.responses import JSONResponse, StreamingResponse from app_dependencies import get_runtime from message_pipeline import encode_sse_data from route_helpers import read_json_request from session_store import ( create_session, delete_session, is_web_session_chat_id, list_web_sessions, load_session_payload, normalize_session_chat_id, rename_session, ) from web_runtime import WebAppRuntime router = APIRouter() @router.get("/events") async def stream_ui_events(request: Request, runtime: WebAppRuntime = Depends(get_runtime)): chat_id = normalize_session_chat_id(str(request.query_params.get("chat_id", "web") or "web")) if not chat_id: return JSONResponse({"error": "invalid chat id"}, status_code=400) subscription_id, queue = await runtime.event_bus.subscribe(chat_id) async def stream_events(): try: yield ": stream-open\n\n" yield encode_sse_data({"type": "events.ready", "chat_id": chat_id}) while True: if await request.is_disconnected(): break try: payload = await asyncio.wait_for(queue.get(), timeout=20.0) except asyncio.TimeoutError: yield ": keepalive\n\n" continue yield encode_sse_data(payload) finally: await runtime.event_bus.unsubscribe(subscription_id) return StreamingResponse( stream_events(), media_type="text/event-stream", headers={ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no", }, ) @router.get("/sessions") async def get_sessions() -> JSONResponse: return JSONResponse({"sessions": list_web_sessions()}) @router.get("/sessions/{chat_id}") async def get_session(chat_id: str) -> JSONResponse: session_chat_id = normalize_session_chat_id(chat_id) if not session_chat_id or not is_web_session_chat_id(session_chat_id): return JSONResponse({"error": "invalid session id"}, status_code=400) payload = load_session_payload(session_chat_id) if payload is None: return JSONResponse({"error": "session not found"}, status_code=404) return JSONResponse(payload) @router.post("/sessions") async def create_session_route( request: Request, runtime: WebAppRuntime = Depends(get_runtime), ) -> JSONResponse: try: payload = await read_json_request(request) except ValueError as exc: return JSONResponse({"error": str(exc)}, status_code=400) title = str(payload.get("title", "")).strip() chat_id = f"web-{uuid.uuid4().hex[:8]}" summary = create_session(chat_id, title=title) await runtime.publish_sessions_changed() return JSONResponse({"session": summary}, status_code=201) @router.patch("/sessions/{chat_id}") async def rename_session_route( chat_id: str, request: Request, runtime: WebAppRuntime = Depends(get_runtime), ) -> JSONResponse: session_chat_id = normalize_session_chat_id(chat_id) if not session_chat_id or not is_web_session_chat_id(session_chat_id): return JSONResponse({"error": "invalid session id"}, status_code=400) try: payload = await read_json_request(request) except ValueError as exc: return JSONResponse({"error": str(exc)}, status_code=400) title = str(payload.get("title", "")).strip() summary = rename_session(session_chat_id, title) if summary is None: return JSONResponse({"error": "session not found"}, status_code=404) await runtime.publish_sessions_changed() return JSONResponse({"session": summary}) @router.delete("/sessions/{chat_id}") async def delete_session_route( chat_id: str, runtime: WebAppRuntime = Depends(get_runtime), ) -> JSONResponse: session_chat_id = normalize_session_chat_id(chat_id) if not session_chat_id or not is_web_session_chat_id(session_chat_id): return JSONResponse({"error": "invalid session id"}, status_code=400) if not delete_session(session_chat_id, runtime.delete_cards_for_chat): return JSONResponse({"error": "session not found"}, status_code=404) await runtime.publish_sessions_changed() await runtime.publish_cards_changed() return JSONResponse({"status": "ok", "chat_id": session_chat_id})