read.markets/app/static/js/chat.js
Giorgio Gilestro e807e58629 ui: fix chat pending class, invented CSS vars, .pf-secondary scope
- chat.js: pending indicator class was wrong (.pending instead of
  chat-msg--pending) so the … waiting message never got italic/dim
- settings.html + cassandra.css: three invented CSS vars (--panel-bg,
  --ok, --surface-1) had hardcoded fallbacks that broke dark mode;
  replaced with real tokens (--surface, --positive)
- cassandra.css: .pf-secondary was scoped to .pf-actions but used
  standalone in 4 places (sync modal, disable-sync, import cancel,
  forget-pie button) — hoisted to a top-level selector

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:19:42 +02:00

74 lines
2.3 KiB
JavaScript

// Cassandra chat sidebar — ephemeral, client-state conversation.
// No persistence: page refresh starts a new chat.
(() => {
const thread = document.getElementById('chat-thread');
const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input');
const send = document.getElementById('chat-send');
if (!thread || !form || !input || !send) return;
/** @type {{role: string, content: string}[]} */
const messages = [];
function escapeHTML(s) {
return s.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function append(role, html_or_text, opts) {
const div = document.createElement('div');
div.className = 'chat-msg chat-msg--' + role;
if (opts && opts.html) {
div.innerHTML = html_or_text;
} else {
div.textContent = html_or_text;
}
thread.appendChild(div);
thread.scrollTop = thread.scrollHeight;
return div;
}
form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = input.value.trim();
if (!text || send.disabled) return;
messages.push({role: 'user', content: text});
append('user', text);
input.value = '';
send.disabled = true;
const thinking = append('assistant', '…');
thinking.classList.add('chat-msg--pending');
try {
const r = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({messages}),
});
if (!r.ok) {
const msg = await r.text();
thinking.className = 'chat-msg chat-msg--error';
thinking.textContent = 'HTTP ' + r.status + ': ' + msg.slice(0, 300);
return;
}
const data = await r.json();
thinking.className = 'chat-msg chat-msg--assistant';
thinking.innerHTML = data.content_html || escapeHTML(data.content);
messages.push({role: 'assistant', content: data.content});
} catch (err) {
thinking.className = 'chat-msg chat-msg--error';
thinking.textContent = 'error: ' + err.message;
} finally {
send.disabled = false;
input.focus();
}
});
// Enter to send; Shift+Enter for newline.
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
form.requestSubmit();
}
});
})();