from pathlib import Path from typing import Any from fastapi import Request, Response, WebSocket, WebSocketDisconnect from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from app.config import Config, settings from app.models import ConversationState, SenderType, TextMessage, User, UserSettings from app.utils import linkify templates = Jinja2Templates(directory=Path(__file__).parent / "templates") templates.env.filters["linkify"] = linkify class IndexHandler: async def handle(self, request: Request, user: User) -> Response: # Get the root path for generating correct URLs behind a proxy root_path = request.scope.get("root_path", "") template = templates.get_template("index.html") html = template.render(root_path=root_path) response = HTMLResponse(html) if user.new_user: response.set_cookie( key="user_id", value=user.id, max_age=60 * 60 * 24, httponly=False, samesite="lax", path="/", ) user.new_user = False return response class WebSocketHandler: def __init__(self, config: Config = settings): self.config = config async def handle(self, web_socket: WebSocket, user: User): await web_socket.accept() await self.send_init_message(web_socket, user) connected = True while connected: try: await self.handle_incoming_message(web_socket, user) except WebSocketDisconnect: connected = False async def send_init_message(self, web_socket: WebSocket, user: User) -> None: payload = user.model_dump(include={"settings", "messages"}) await web_socket.send_json({"type": "init", "payload": payload}) async def handle_incoming_message(self, web_socket: WebSocket, user: User) -> None: data = await web_socket.receive_json() msg_type = data.get("type") if msg_type == "chat": await self.handle_chat_message(web_socket, user, data) elif msg_type == "reset": await self.handle_reset_message(web_socket, user) elif msg_type == "settings": await self.handle_settings_message(web_socket, user, data) elif msg_type == "ping": await web_socket.send_json({"type": "pong", "payload": {"ts": data.get("ts")}}) async def handle_reset_message(self, web_socket: WebSocket, user: User) -> None: # Only clear messages and reset conversation state; preserve existing user.settings user.messages = [] user.connector.fansifter_connector.state = ConversationState.GREETING payload = user.model_dump(include={"settings", "messages"}) await web_socket.send_json({"type": "reset", "payload": payload}) async def handle_settings_message( self, web_socket: WebSocket, user: User, data: dict[str, Any] ) -> None: incoming_settings: dict[str, Any] = data.get("settings", {}) try: user.settings = UserSettings.model_validate(incoming_settings) except Exception: await web_socket.send_json( { "type": "error", "payload": { "sender": "system", "event": "settings update error", }, } ) return await web_socket.send_json( { "type": "settings_ack", "payload": user.model_dump(include={"settings"}), } ) async def handle_chat_message( self, web_socket: WebSocket, user: User, data: dict[str, Any] ) -> None: text = str(data.get("text", "")).strip() if not text: return fan_msg = TextMessage(sender=SenderType.fan, text=text) user.messages.append(fan_msg) fan_payload = fan_msg.model_dump() await web_socket.send_json({"type": "chat", "payload": fan_payload}) state = user.state.value produced = await user.connector.handle_incoming_message(user, text) if state != user.state.value: await web_socket.send_json( { "type": "log_event", "payload": { "sender": "fansifter", "event": f"state changed to {user.state.value}", }, } ) for message in produced: user.messages.append(message) payload_msg = message.model_dump() await web_socket.send_json({"type": "chat", "payload": payload_msg}) if user.settings.app_settings.show_events: await web_socket.send_json( { "type": "log_event", "payload": { "sender": message.sender, "event": "reply sent", }, } )