- 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>
74 lines
2.3 KiB
JavaScript
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>');
|
|
}
|
|
|
|
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();
|
|
}
|
|
});
|
|
})();
|