121 lines
4.0 KiB
HTML
121 lines
4.0 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}AI Coach{% endblock %}
|
|
{% set hide_sidebar = True %}
|
|
{% block content %}
|
|
<h1>AI Coach</h1>
|
|
|
|
<div class="chat-messages" id="chat-messages">
|
|
{% for m in messages %}
|
|
<div class="chat-message {{ m.role }}">
|
|
<small>{{ m.role }} · {{ m.created_at[:10] if m.created_at else '' }}</small>
|
|
<p>{{ m.content }}</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<form class="chat-input" id="chat-form" onsubmit="sendMessage(event)"
|
|
data-first="{{ first }}" data-onboarding="{{ onboarding_prompt|e }}">
|
|
<textarea id="chat-input" placeholder="Ask your coach..." rows="3" required autocomplete="off"></textarea>
|
|
<button type="submit" id="chat-send-btn">Send</button>
|
|
</form>
|
|
<p id="chat-status" class="chat-status"></p>
|
|
|
|
<script>
|
|
const chatMessages = document.getElementById('chat-messages');
|
|
const chatInput = document.getElementById('chat-input');
|
|
const chatForm = document.getElementById('chat-form');
|
|
const chatSendBtn = document.getElementById('chat-send-btn');
|
|
const chatStatus = document.getElementById('chat-status');
|
|
|
|
const IS_FIRST = chatForm.dataset.first === '1';
|
|
const ONBOARDING_PROMPT = chatForm.dataset.onboarding;
|
|
|
|
chatInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
chatForm.dispatchEvent(new Event('submit'));
|
|
}
|
|
});
|
|
|
|
function appendMessage(role, content) {
|
|
const div = document.createElement('div');
|
|
div.className = 'chat-message ' + role;
|
|
div.innerHTML = '<small>' + role + '</small><p>' + content + '</p>';
|
|
chatMessages.appendChild(div);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function setStatus(msg, isError) {
|
|
chatStatus.textContent = msg;
|
|
chatStatus.style.color = isError ? 'var(--del-color, #b33)' : 'var(--muted-color, #888)';
|
|
}
|
|
|
|
async function sendMessage(event) {
|
|
event.preventDefault();
|
|
const text = chatInput.value;
|
|
if (!text.trim()) return;
|
|
|
|
appendMessage('user', text);
|
|
chatInput.value = '';
|
|
chatInput.disabled = true;
|
|
chatSendBtn.disabled = true;
|
|
setStatus('Waiting for coach...');
|
|
|
|
const assistantDiv = document.createElement('div');
|
|
assistantDiv.className = 'chat-message assistant';
|
|
assistantDiv.innerHTML = '<small>assistant</small><p><em>Thinking...</em></p>';
|
|
chatMessages.appendChild(assistantDiv);
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
|
|
const formData = new FormData();
|
|
formData.append('message', text);
|
|
|
|
try {
|
|
const response = await fetch('/chat', { method: 'POST', body: formData });
|
|
if (!response.ok) {
|
|
throw new Error('Server returned ' + response.status);
|
|
}
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
const p = assistantDiv.querySelector('p');
|
|
p.innerHTML = '';
|
|
|
|
let buffer = '';
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
const lines = buffer.split('\n');
|
|
buffer = lines.pop() || '';
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
p.innerHTML += line.slice(6);
|
|
}
|
|
}
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
if (!p.innerHTML.trim()) {
|
|
p.innerHTML = '<em>No response received. Make sure the AI coach is running (<code>opencode serve</code>).</em>';
|
|
}
|
|
|
|
setStatus('');
|
|
chatInput.disabled = false;
|
|
chatSendBtn.disabled = false;
|
|
chatInput.focus();
|
|
} catch (err) {
|
|
assistantDiv.querySelector('p').innerHTML = '<em>Error connecting to coach.</em>';
|
|
setStatus('Could not reach the AI coach. Is <code>opencode serve</code> running?', true);
|
|
chatInput.disabled = false;
|
|
chatSendBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
if (IS_FIRST && ONBOARDING_PROMPT) {
|
|
chatInput.value = ONBOARDING_PROMPT;
|
|
chatForm.dispatchEvent(new Event('submit'));
|
|
}
|
|
</script>
|
|
{% endblock %}
|