240 lines
11 KiB
HTML
240 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}AI Assistant{% endblock %}
|
|
{% block page_title %}AI Assistant{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
#chatMessages { height: 440px; overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding: 16px; scroll-behavior: smooth; }
|
|
.msg-user { align-self: flex-end; max-width: 75%; background: #3b82f6; color: #fff; border-radius: 16px 16px 4px 16px; padding: 10px 14px; font-size: 13.5px; line-height: 1.5; }
|
|
.msg-ai { align-self: flex-start; max-width: 82%; background: #1e293b; color: #e2e8f0; border-radius: 16px 16px 16px 4px; padding: 10px 14px; font-size: 13.5px; line-height: 1.6; }
|
|
.msg-ai.streaming { border-left: 2px solid #818cf8; }
|
|
.msg-system { align-self: center; font-size: 12px; color: var(--muted); font-style: italic; }
|
|
.suggestion-btn { background: #f1f5f9; border: 1px solid var(--border); border-radius: 20px; padding: 6px 14px; font-size: 12px; color: var(--text); cursor: pointer; transition: all .15s; white-space: nowrap; }
|
|
.suggestion-btn:hover { background: #e2e8f0; border-color: #94a3b8; }
|
|
.typing-dot { width: 6px; height: 6px; border-radius: 50%; background: #818cf8; display: inline-block; animation: bounce .8s infinite; }
|
|
.typing-dot:nth-child(2) { animation-delay: .15s; }
|
|
.typing-dot:nth-child(3) { animation-delay: .3s; }
|
|
@keyframes bounce { 0%,60%,100% { transform: translateY(0); } 30% { transform: translateY(-6px); } }
|
|
{% endblock %}
|
|
|
|
{% block topbar_actions %}
|
|
<a href="{{ url_for('ai.history') }}" class="btn btn-sm btn-outline-secondary" style="font-size:12px;"><i class="bi bi-clock-history me-1"></i>History</a>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row g-3">
|
|
<!-- Chat panel -->
|
|
<div class="col-12 col-lg-8">
|
|
<div class="pcard p-0 d-flex flex-column" style="height:600px;background:#0f172a;border-color:#1e293b;">
|
|
<!-- Header -->
|
|
<div style="padding:14px 16px;border-bottom:1px solid #1e293b;display:flex;align-items:center;gap:10px;">
|
|
<div style="width:32px;height:32px;border-radius:8px;background:#312e81;display:flex;align-items:center;justify-content:center;">
|
|
<i class="bi bi-stars" style="color:#818cf8;font-size:15px;"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:13px;font-weight:600;color:#f1f5f9;">Finance AI</div>
|
|
<div style="font-size:11px;color:#475569;" id="statusLine">Powered by Groq · Context: last 90 days</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
<div id="chatMessages">
|
|
<div class="msg-system">Ask me anything about your finances.</div>
|
|
{% if recent_chats %}
|
|
{% for chat in recent_chats|reverse %}
|
|
<div class="msg-user">{{ chat.prompt_summary }}</div>
|
|
<div class="msg-ai">{{ chat.content | replace('\n', '<br>') | safe }}</div>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Input -->
|
|
<div style="padding:12px 16px;border-top:1px solid #1e293b;margin-top:auto;">
|
|
<div style="display:flex;gap:8px;align-items:flex-end;">
|
|
<textarea id="chatInput" rows="2"
|
|
style="flex:1;background:#1e293b;border:1px solid #334155;border-radius:10px;color:#f1f5f9;font-size:13.5px;padding:10px 14px;resize:none;font-family:'DM Sans',sans-serif;outline:none;transition:border .2s;"
|
|
placeholder="Ask about your spending, budgets, goals…"
|
|
onfocus="this.style.borderColor='#818cf8'" onblur="this.style.borderColor='#334155'"></textarea>
|
|
<button id="sendBtn" onclick="sendMessage()"
|
|
style="width:40px;height:40px;border-radius:10px;background:#4f46e5;border:none;color:#fff;cursor:pointer;font-size:16px;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .15s;">
|
|
<i class="bi bi-send"></i>
|
|
</button>
|
|
</div>
|
|
<div style="font-size:11px;color:#334155;margin-top:6px;">Enter to send · Shift+Enter for new line</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right column: daily insight + suggestions -->
|
|
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
|
|
|
|
<!-- Daily insight -->
|
|
{% if latest_insight %}
|
|
<div class="pcard" style="background:#0f172a;border-color:#1e293b;">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<span style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#818cf8;">
|
|
<i class="bi bi-stars me-1"></i>Daily Insight
|
|
</span>
|
|
<span style="font-size:10px;color:#475569;">{{ latest_insight.insight_date.strftime('%b %d') }}</span>
|
|
</div>
|
|
<p style="font-size:13px;color:#94a3b8;line-height:1.6;margin:0;">{{ latest_insight.content }}</p>
|
|
<form method="POST" action="{{ url_for('ai.trigger_insight') }}" class="mt-2">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" style="font-size:11px;background:none;border:none;color:#475569;cursor:pointer;padding:0;" title="Regenerate today's insight">
|
|
<i class="bi bi-arrow-clockwise me-1"></i>Regenerate
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div class="pcard" style="background:#0f172a;border-color:#1e293b;">
|
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#475569;margin-bottom:8px;">Daily Insight</div>
|
|
<p style="font-size:13px;color:#64748b;margin-bottom:10px;">No insight generated yet.</p>
|
|
<form method="POST" action="{{ url_for('ai.trigger_insight') }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm" style="background:#312e81;color:#818cf8;border:none;font-size:12px;">
|
|
<i class="bi bi-stars me-1"></i>Generate Now
|
|
</button>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Suggested questions -->
|
|
<div class="pcard" style="background:#0f172a;border-color:#1e293b;">
|
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#475569;margin-bottom:10px;">Suggested Questions</div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
{% set suggestions = [
|
|
"Where did I overspend this month?",
|
|
"How is my budget looking?",
|
|
"Am I on track for my goals?",
|
|
"What's my biggest expense category?",
|
|
"Summarize my finances",
|
|
"How can I save more?",
|
|
"What's my net worth trend?",
|
|
"Review my investments",
|
|
] %}
|
|
{% for s in suggestions %}
|
|
<button class="suggestion-btn" onclick="fillSuggestion(this.textContent)">{{ s }}</button>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tips -->
|
|
<div class="pcard" style="background:#0f172a;border-color:#1e293b;">
|
|
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#475569;margin-bottom:8px;">Tips</div>
|
|
<ul style="font-size:12px;color:#64748b;padding-left:16px;margin:0;line-height:1.8;">
|
|
<li>AI sees your last 90 days of transactions</li>
|
|
<li>Budget alerts and goal progress are included</li>
|
|
<li>No personal names are sent to the AI</li>
|
|
<li>Daily insights auto-generate at midnight</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
const chatMessages = document.getElementById('chatMessages');
|
|
const chatInput = document.getElementById('chatInput');
|
|
const sendBtn = document.getElementById('sendBtn');
|
|
const statusLine = document.getElementById('statusLine');
|
|
|
|
// Enter to send
|
|
chatInput.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
sendMessage();
|
|
}
|
|
});
|
|
|
|
function fillSuggestion(text) {
|
|
chatInput.value = text;
|
|
chatInput.focus();
|
|
}
|
|
|
|
function scrollBottom() {
|
|
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
}
|
|
|
|
function addMessage(text, type) {
|
|
const div = document.createElement('div');
|
|
div.className = 'msg-' + type;
|
|
if (type === 'ai') div.innerHTML = text;
|
|
else div.textContent = text;
|
|
chatMessages.appendChild(div);
|
|
scrollBottom();
|
|
return div;
|
|
}
|
|
|
|
function sendMessage() {
|
|
const msg = chatInput.value.trim();
|
|
if (!msg) return;
|
|
|
|
chatInput.value = '';
|
|
chatInput.disabled = true;
|
|
sendBtn.disabled = true;
|
|
sendBtn.innerHTML = '<i class="bi bi-hourglass-split"></i>';
|
|
|
|
// User bubble
|
|
addMessage(msg, 'user');
|
|
|
|
// Typing indicator
|
|
const typingDiv = document.createElement('div');
|
|
typingDiv.className = 'msg-ai';
|
|
typingDiv.innerHTML = '<span class="typing-dot"></span><span class="typing-dot"></span><span class="typing-dot"></span>';
|
|
chatMessages.appendChild(typingDiv);
|
|
scrollBottom();
|
|
|
|
statusLine.textContent = 'Thinking…';
|
|
|
|
// SSE stream
|
|
const url = '/ai/stream?message=' + encodeURIComponent(msg);
|
|
const evtSource = new EventSource(url);
|
|
let aiDiv = null;
|
|
let buffer = '';
|
|
let first = true;
|
|
|
|
evtSource.onmessage = function(e) {
|
|
if (e.data === '[DONE]') {
|
|
evtSource.close();
|
|
chatInput.disabled = false;
|
|
sendBtn.disabled = false;
|
|
sendBtn.innerHTML = '<i class="bi bi-send"></i>';
|
|
statusLine.textContent = 'Powered by Groq · Context: last 90 days';
|
|
chatInput.focus();
|
|
return;
|
|
}
|
|
|
|
if (first) {
|
|
typingDiv.remove();
|
|
aiDiv = document.createElement('div');
|
|
aiDiv.className = 'msg-ai streaming';
|
|
chatMessages.appendChild(aiDiv);
|
|
first = false;
|
|
}
|
|
|
|
// Replace <br> back to newlines for rendering
|
|
const chunk = e.data.replace(/<br>/g, '\n');
|
|
buffer += chunk;
|
|
aiDiv.innerHTML = buffer.replace(/\n/g, '<br>');
|
|
aiDiv.classList.remove('streaming');
|
|
scrollBottom();
|
|
};
|
|
|
|
evtSource.onerror = function() {
|
|
evtSource.close();
|
|
typingDiv.remove();
|
|
if (!aiDiv) {
|
|
addMessage('Connection error. Please try again.', 'system');
|
|
}
|
|
chatInput.disabled = false;
|
|
sendBtn.disabled = false;
|
|
sendBtn.innerHTML = '<i class="bi bi-send"></i>';
|
|
statusLine.textContent = 'Powered by Groq · Context: last 90 days';
|
|
};
|
|
}
|
|
|
|
// Scroll to bottom on load
|
|
scrollBottom();
|
|
</script>
|
|
{% endblock %}
|