Fix chat UI, add weekly plan view to dashboard
- Chat: textarea instead of single-line input, compact send button - Chat: show status messages and better error handling - Dashboard: new 'This Week' section showing planned workouts - Chat: Enter sends, Shift+Enter adds newline
This commit is contained in:
parent
87a5da6f03
commit
0b67939a53
@ -1,3 +1,4 @@
|
||||
from datetime import date, timedelta
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
@ -13,6 +14,13 @@ router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
def _week_bounds() -> tuple[str, str]:
|
||||
today = date.today()
|
||||
monday = today - timedelta(days=today.weekday())
|
||||
sunday = monday + timedelta(days=6)
|
||||
return monday.isoformat(), sunday.isoformat()
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def root_redirect():
|
||||
from fastapi.responses import RedirectResponse
|
||||
@ -21,6 +29,8 @@ async def root_redirect():
|
||||
|
||||
@router.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard(request: Request, user: User = Depends(get_current_user)):
|
||||
week_start, week_end = _week_bounds()
|
||||
|
||||
async with async_session() as session:
|
||||
result = await session.execute(
|
||||
select(Workout)
|
||||
@ -43,9 +53,23 @@ async def dashboard(request: Request, user: User = Depends(get_current_user)):
|
||||
)
|
||||
current_phase = result.scalar_one_or_none()
|
||||
|
||||
result = await session.execute(
|
||||
select(Workout)
|
||||
.where(
|
||||
Workout.user_id == user.id,
|
||||
Workout.date >= week_start,
|
||||
Workout.date <= week_end,
|
||||
)
|
||||
.order_by(Workout.date)
|
||||
)
|
||||
this_week = result.scalars().all()
|
||||
|
||||
return templates.TemplateResponse(request, "dashboard.html", {
|
||||
"user": user,
|
||||
"recent_workouts": recent_workouts,
|
||||
"latest_checkin": latest_checkin,
|
||||
"current_phase": current_phase,
|
||||
"this_week": this_week,
|
||||
"week_start": week_start,
|
||||
"week_end": week_end,
|
||||
})
|
||||
|
||||
@ -37,10 +37,24 @@
|
||||
.chat-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chat-input input {
|
||||
.chat-input textarea {
|
||||
flex: 1;
|
||||
resize: vertical;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.chat-input button {
|
||||
width: auto;
|
||||
padding: 0.5rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-status {
|
||||
margin-top: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
|
||||
@ -13,38 +13,64 @@
|
||||
</div>
|
||||
|
||||
<form class="chat-input" id="chat-form" onsubmit="sendMessage(event)">
|
||||
<input type="text" id="chat-input" placeholder="Ask your coach..." required autocomplete="off">
|
||||
<button type="submit">Send</button>
|
||||
<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>
|
||||
function sendMessage(event) {
|
||||
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');
|
||||
|
||||
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 input = document.getElementById('chat-input');
|
||||
const messages = document.getElementById('chat-messages');
|
||||
const text = input.value;
|
||||
const text = chatInput.value;
|
||||
if (!text.trim()) return;
|
||||
|
||||
const userDiv = document.createElement('div');
|
||||
userDiv.className = 'chat-message user';
|
||||
userDiv.innerHTML = `<small>user</small><p>${text}</p>`;
|
||||
messages.appendChild(userDiv);
|
||||
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>';
|
||||
messages.appendChild(assistantDiv);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
input.disabled = true;
|
||||
chatMessages.appendChild(assistantDiv);
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('message', text);
|
||||
|
||||
fetch('/chat', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
}).then(async response => {
|
||||
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');
|
||||
@ -62,16 +88,23 @@ function sendMessage(event) {
|
||||
p.innerHTML += line.slice(6);
|
||||
}
|
||||
}
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
||||
}
|
||||
|
||||
input.disabled = false;
|
||||
input.value = '';
|
||||
input.focus();
|
||||
}).catch(() => {
|
||||
assistantDiv.querySelector('p').innerHTML = 'Error connecting to coach.';
|
||||
input.disabled = false;
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -27,6 +27,32 @@
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<h2>This Week ({{ week_start }} — {{ week_end }})</h2>
|
||||
{% if this_week %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Day</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for w in this_week %}
|
||||
<tr>
|
||||
<td>{{ w.date }}</td>
|
||||
<td>{{ w.name }}</td>
|
||||
<td>{{ w.status }}</td>
|
||||
<td><a href="/workouts/{{ w.id }}">View</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No workouts planned this week. Ask the <a href="/chat">AI Coach</a> to plan your week.</p>
|
||||
{% endif %}
|
||||
|
||||
{% if latest_checkin %}
|
||||
<article>
|
||||
<h3>Latest Morning Check-in</h3>
|
||||
@ -35,7 +61,7 @@
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
<h2>Recent Workouts</h2>
|
||||
<h2>Past Workouts</h2>
|
||||
{% if recent_workouts %}
|
||||
<table>
|
||||
<thead>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user