from datetime import datetime, timezone import uuid from fastapi import APIRouter, Request, Depends, Form from fastapi.responses import HTMLResponse, StreamingResponse from fastapi.templating import Jinja2Templates from sqlalchemy import select, desc from app.models.base import async_session from app.models.user import User from app.models.chat import ChatMessage from app.models.workout import Workout from app.models.checkin import Checkin from app.auth import get_current_user from app.services.opencode_proxy import query_opencode router = APIRouter() templates = Jinja2Templates(directory="app/templates") @router.get("/chat", response_class=HTMLResponse) async def chat_page(request: Request, user: User = Depends(get_current_user)): session_id = request.cookies.get("chat_session_id") if not session_id: session_id = str(uuid.uuid4()) async with async_session() as session: result = await session.execute( select(ChatMessage) .where( ChatMessage.user_id == user.id, ChatMessage.session_id == session_id, ) .order_by(ChatMessage.created_at) ) messages = result.scalars().all() resp = templates.TemplateResponse(request, "chat.html", { "user": user, "messages": messages, "session_id": session_id, }) resp.set_cookie(key="chat_session_id", value=session_id, httponly=True, max_age=86400 * 30) return resp @router.post("/chat") async def chat_send( request: Request, user: User = Depends(get_current_user), message: str = Form(), ): session_id = request.cookies.get("chat_session_id") or str(uuid.uuid4()) async with async_session() as session: result = await session.execute( select(Workout) .where(Workout.user_id == user.id) .order_by(desc(Workout.date)) .limit(5) ) recent_workouts = result.scalars().all() result = await session.execute( select(Checkin) .where(Checkin.user_id == user.id) .order_by(desc(Checkin.date)) .limit(5) ) recent_checkins = result.scalars().all() workout_lines = [] for w in recent_workouts: workout_lines.append(f" {w.date} — {w.name} ({w.status})") checkin_lines = [] for c in recent_checkins: parts = [] if c.feeling: parts.append(f"feeling={c.feeling}") if c.weight_lb: parts.append(f"weight={c.weight_lb}lb") if c.calories: parts.append(f"cal(yesterday)={c.calories}") if c.steps: parts.append(f"steps(yesterday)={c.steps}") if c.sleep_hours: parts.append(f"sleep={c.sleep_hours}h") checkin_lines.append(f" {c.date} — {' | '.join(parts)}") user_context = ( f"Username: {user.username}. " f"Weight: {user.weight_lb} lb. " f"Goals: {user.goals or 'Not specified'}. " f"Equipment: {user.equipment or 'Not specified'}. " f"Medical: {user.medical_notes or 'None'}. " f"Calorie goal: {user.calorie_goal or 'Not set'}. " f"Step goal: {user.step_goal or 'Not set'}. " ) if recent_workouts: user_context += "Recent workouts:\n" + "\n".join(workout_lines) + ". " if recent_checkins: user_context += "Recent check-ins:\n" + "\n".join(checkin_lines) + ". " async def stream(): async with async_session() as session: user_msg = ChatMessage( user_id=user.id, session_id=session_id, role="user", content=message, created_at=datetime.now(timezone.utc).isoformat(), ) session.add(user_msg) await session.commit() assistant_content = "" async for chunk in query_opencode(message, session_id, user_context): assistant_content += chunk yield f"data: {chunk}\n\n" async with async_session() as session: assistant_msg = ChatMessage( user_id=user.id, session_id=session_id, role="assistant", content=assistant_content, created_at=datetime.now(timezone.utc).isoformat(), ) session.add(assistant_msg) await session.commit() resp = StreamingResponse(stream(), media_type="text/event-stream") resp.set_cookie(key="chat_session_id", value=session_id, httponly=True, max_age=86400 * 30) return resp