Fix form submit cancelability, improve opencode error handling
This commit is contained in:
parent
4a8a071b86
commit
2aca0e5d42
@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
import uuid
|
import uuid
|
||||||
from fastapi import APIRouter, Request, Depends, Form
|
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.checkin import Checkin
|
||||||
from app.models.measurement import Measurement, MeasurementType
|
from app.models.measurement import Measurement, MeasurementType
|
||||||
from app.auth import get_current_user
|
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()
|
router = APIRouter()
|
||||||
templates = Jinja2Templates(directory="app/templates")
|
templates = Jinja2Templates(directory="app/templates")
|
||||||
@ -26,6 +27,9 @@ ONBOARDING_PROMPT = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger("chat")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/api/chat/messages")
|
@router.get("/api/chat/messages")
|
||||||
async def get_chat_messages(request: Request, user: User = Depends(get_current_user)):
|
async def get_chat_messages(request: Request, user: User = Depends(get_current_user)):
|
||||||
session_id = request.cookies.get("chat_session_id")
|
session_id = request.cookies.get("chat_session_id")
|
||||||
@ -159,9 +163,15 @@ async def chat_send(
|
|||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
assistant_content = ""
|
assistant_content = ""
|
||||||
async for chunk in query_opencode(message, session_id, user_context):
|
try:
|
||||||
assistant_content += chunk
|
async for chunk in query_opencode(message, session_id, user_context):
|
||||||
yield f"data: {chunk}\n\n"
|
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:
|
async with async_session() as session:
|
||||||
assistant_msg = ChatMessage(
|
assistant_msg = ChatMessage(
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
from app.config import OPENCODE_SERVE_URL
|
from app.config import OPENCODE_SERVE_URL
|
||||||
|
|
||||||
|
logger = logging.getLogger("opencode_proxy")
|
||||||
|
|
||||||
|
|
||||||
|
class OpenCodeUnavailableError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def query_opencode(
|
async def query_opencode(
|
||||||
message: str,
|
message: str,
|
||||||
@ -37,6 +44,10 @@ async def query_opencode(
|
|||||||
|
|
||||||
await proc.wait()
|
await proc.wait()
|
||||||
except FileNotFoundError:
|
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:
|
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}")
|
||||||
|
|||||||
@ -79,7 +79,12 @@
|
|||||||
buffer = lines.pop() || '';
|
buffer = lines.pop() || '';
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
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;
|
messagesEl.scrollTop = messagesEl.scrollHeight;
|
||||||
@ -104,7 +109,9 @@
|
|||||||
input.addEventListener('keydown', function(e) {
|
input.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
form.dispatchEvent(new Event('submit'));
|
const event = new Event('submit', {cancelable: true});
|
||||||
|
form.dispatchEvent(event);
|
||||||
|
if (!event.defaultPrevented) sendMessage(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,9 @@ const ONBOARDING_PROMPT = chatForm.dataset.onboarding;
|
|||||||
chatInput.addEventListener('keydown', function(e) {
|
chatInput.addEventListener('keydown', function(e) {
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
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() || '';
|
buffer = lines.pop() || '';
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
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;
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||||
@ -114,7 +121,9 @@ async function sendMessage(event) {
|
|||||||
|
|
||||||
if (IS_FIRST && ONBOARDING_PROMPT) {
|
if (IS_FIRST && ONBOARDING_PROMPT) {
|
||||||
chatInput.value = 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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user