Fix form submit cancelability, improve opencode error handling

This commit is contained in:
Jacob Hinkle 2026-06-29 11:53:08 -04:00
parent 4a8a071b86
commit 2aca0e5d42
4 changed files with 48 additions and 11 deletions

View File

@ -1,3 +1,4 @@
import logging
from datetime import datetime, timezone
import uuid
from fastapi import APIRouter, Request, Depends, Form
@ -12,7 +13,7 @@ from app.models.workout import Workout
from app.models.checkin import Checkin
from app.models.measurement import Measurement, MeasurementType
from app.auth import get_current_user
from app.services.opencode_proxy import query_opencode
from app.services.opencode_proxy import query_opencode, OpenCodeUnavailableError
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@ -26,6 +27,9 @@ ONBOARDING_PROMPT = (
)
logger = logging.getLogger("chat")
@router.get("/api/chat/messages")
async def get_chat_messages(request: Request, user: User = Depends(get_current_user)):
session_id = request.cookies.get("chat_session_id")
@ -159,9 +163,15 @@ async def chat_send(
await session.commit()
assistant_content = ""
try:
async for chunk in query_opencode(message, session_id, user_context):
assistant_content += chunk
yield f"data: {chunk}\n\n"
except OpenCodeUnavailableError as e:
logger.error("Chat failed for user %s: %s", user.username, e)
yield f"data: [error] {e}\n\n"
# Don't save an error assistant message
return
async with async_session() as session:
assistant_msg = ChatMessage(

View File

@ -1,8 +1,15 @@
import asyncio
import json
import logging
from typing import AsyncGenerator
from app.config import OPENCODE_SERVE_URL
logger = logging.getLogger("opencode_proxy")
class OpenCodeUnavailableError(Exception):
pass
async def query_opencode(
message: str,
@ -37,6 +44,10 @@ async def query_opencode(
await proc.wait()
except FileNotFoundError:
yield "AI coach is not available. Install opencode and run `opencode serve` to enable."
logger.error("opencode binary not found in PATH")
raise OpenCodeUnavailableError(
"opencode binary not found. Install opencode or run `opencode serve`."
)
except Exception as e:
yield f"Error connecting to AI coach: {e}"
logger.error("opencode proxy error: %s", e)
raise OpenCodeUnavailableError(f"AI coach unavailable: {e}")

View File

@ -79,7 +79,12 @@
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
p.innerHTML += line.slice(6);
const chunk = line.slice(6);
if (chunk.startsWith('[error] ')) {
p.innerHTML = '<strong style="color: var(--del-color, #b33)">' + chunk.slice(8) + '</strong>';
break;
}
p.innerHTML += chunk;
}
}
messagesEl.scrollTop = messagesEl.scrollHeight;
@ -104,7 +109,9 @@
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
form.dispatchEvent(new Event('submit'));
const event = new Event('submit', {cancelable: true});
form.dispatchEvent(event);
if (!event.defaultPrevented) sendMessage(event);
}
});
}

View File

@ -33,7 +33,9 @@ const ONBOARDING_PROMPT = chatForm.dataset.onboarding;
chatInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
chatForm.dispatchEvent(new Event('submit'));
const event = new Event('submit', {cancelable: true});
chatForm.dispatchEvent(event);
if (!event.defaultPrevented) sendMessage(event);
}
});
@ -90,7 +92,12 @@ async function sendMessage(event) {
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
p.innerHTML += line.slice(6);
const chunk = line.slice(6);
if (chunk.startsWith('[error] ')) {
p.innerHTML = '<strong style="color: var(--del-color, #b33)">' + chunk.slice(8) + '</strong>';
break;
}
p.innerHTML += chunk;
}
}
chatMessages.scrollTop = chatMessages.scrollHeight;
@ -114,7 +121,9 @@ async function sendMessage(event) {
if (IS_FIRST && ONBOARDING_PROMPT) {
chatInput.value = ONBOARDING_PROMPT;
chatForm.dispatchEvent(new Event('submit'));
const event = new Event('submit', {cancelable: true});
chatForm.dispatchEvent(event);
if (!event.defaultPrevented) sendMessage(event);
}
</script>
{% endblock %}