feat(gateway-ui): rewrite all page renderers with Tailwind classes
Convert dashboard, chat, sessions, usage, and settings pages from legacy CSS to Tailwind utility classes. Responsive grid layouts, mobile-friendly touch targets, zinc/blue color palette. All element IDs and event bindings preserved for functional compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,7 +36,7 @@ function escapeHtml(text) {
|
|||||||
|
|
||||||
function highlightCode() {
|
function highlightCode() {
|
||||||
if (typeof hljs !== 'undefined') {
|
if (typeof hljs !== 'undefined') {
|
||||||
document.querySelectorAll('.chat-messages pre code').forEach(block => {
|
document.querySelectorAll('#chat-messages pre code').forEach(block => {
|
||||||
hljs.highlightElement(block);
|
hljs.highlightElement(block);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -44,10 +44,26 @@ function highlightCode() {
|
|||||||
|
|
||||||
function createMessageEl(role, content) {
|
function createMessageEl(role, content) {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'message-wrapper';
|
|
||||||
|
const roleClasses = {
|
||||||
|
user: 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-end group',
|
||||||
|
assistant: 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-start group',
|
||||||
|
system: 'flex flex-col gap-1.5 self-stretch max-w-full group',
|
||||||
|
error: 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-start group',
|
||||||
|
};
|
||||||
|
wrapper.className = roleClasses[role] || roleClasses.assistant;
|
||||||
|
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = `message ${role}`;
|
|
||||||
|
const messageClasses = {
|
||||||
|
user: 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-blue-500/15 border border-blue-500/25 text-zinc-50',
|
||||||
|
assistant: 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50',
|
||||||
|
system: 'rounded-lg px-3.5 py-2.5 leading-relaxed break-words whitespace-pre-wrap bg-zinc-800 text-zinc-400 border border-zinc-700 text-xs',
|
||||||
|
error: 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-red-500/15 border border-red-500/30 text-zinc-50',
|
||||||
|
};
|
||||||
|
div.className = messageClasses[role] || messageClasses.assistant;
|
||||||
|
// Keep role as a data attribute for querySelector compatibility
|
||||||
|
div.dataset.role = role;
|
||||||
|
|
||||||
if (role === 'assistant' || role === 'system') {
|
if (role === 'assistant' || role === 'system') {
|
||||||
div.innerHTML = renderSafeMarkdown(content);
|
div.innerHTML = renderSafeMarkdown(content);
|
||||||
@@ -67,16 +83,16 @@ function createMessageEl(role, content) {
|
|||||||
|
|
||||||
function createToolEventEl(event, data) {
|
function createToolEventEl(event, data) {
|
||||||
const group = document.createElement('div');
|
const group = document.createElement('div');
|
||||||
group.className = 'tool-event-group';
|
group.className = 'border border-zinc-800 rounded-md my-1 overflow-hidden';
|
||||||
|
|
||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.className = 'tool-event-header';
|
header.className = 'flex items-center gap-2 px-3 py-1.5 bg-zinc-800 cursor-pointer text-xs text-zinc-500 hover:text-zinc-400 select-none';
|
||||||
|
|
||||||
if (event === 'tool_start') {
|
if (event === 'tool_start') {
|
||||||
header.innerHTML = `<span class="spinner"></span> <strong>${escapeHtml(data.tool)}</strong>`;
|
header.innerHTML = `<span class="spinner"></span> <strong>${escapeHtml(data.tool)}</strong>`;
|
||||||
} else if (event === 'tool_end') {
|
} else if (event === 'tool_end') {
|
||||||
const icon = data.result?.success ? '✓' : '✗';
|
const icon = data.result?.success ? '✓' : '✗';
|
||||||
const cls = data.result?.success ? 'status-ok' : 'status-error';
|
const cls = data.result?.success ? 'text-green-500' : 'text-red-500';
|
||||||
header.innerHTML = `<span class="${cls}">${icon}</span> <strong>${escapeHtml(data.tool)}</strong>`;
|
header.innerHTML = `<span class="${cls}">${icon}</span> <strong>${escapeHtml(data.tool)}</strong>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +101,7 @@ function createToolEventEl(event, data) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'tool-event-body';
|
body.className = 'tool-event-body px-3 py-2 text-xs text-zinc-400 bg-zinc-900 whitespace-pre-wrap break-all max-h-48 overflow-y-auto font-mono';
|
||||||
|
|
||||||
if (event === 'tool_start' && data.args) {
|
if (event === 'tool_start' && data.args) {
|
||||||
body.textContent = JSON.stringify(data.args, null, 2);
|
body.textContent = JSON.stringify(data.args, null, 2);
|
||||||
@@ -144,17 +160,17 @@ function renderPendingAttachments() {
|
|||||||
for (let i = 0; i < _pendingAttachments.length; i++) {
|
for (let i = 0; i < _pendingAttachments.length; i++) {
|
||||||
const att = _pendingAttachments[i];
|
const att = _pendingAttachments[i];
|
||||||
const chip = document.createElement('div');
|
const chip = document.createElement('div');
|
||||||
chip.className = 'attachment-chip';
|
chip.className = 'inline-flex items-center gap-2 px-2.5 py-1 rounded-full bg-zinc-900 border border-zinc-800 text-zinc-400 text-xs';
|
||||||
|
|
||||||
const name = document.createElement('span');
|
const name = document.createElement('span');
|
||||||
name.className = 'attachment-name';
|
name.className = 'max-w-[220px] overflow-hidden text-ellipsis whitespace-nowrap';
|
||||||
name.textContent = att.filename || 'attachment';
|
name.textContent = att.filename || 'attachment';
|
||||||
|
|
||||||
const rm = document.createElement('button');
|
const rm = document.createElement('button');
|
||||||
rm.className = 'attachment-remove';
|
rm.className = 'text-zinc-500 hover:text-zinc-50 cursor-pointer text-base leading-none appearance-none border-0 bg-transparent';
|
||||||
rm.type = 'button';
|
rm.type = 'button';
|
||||||
rm.title = 'Remove attachment';
|
rm.title = 'Remove attachment';
|
||||||
rm.textContent = '×';
|
rm.textContent = '\u00d7';
|
||||||
rm.addEventListener('click', () => {
|
rm.addEventListener('click', () => {
|
||||||
_pendingAttachments.splice(i, 1);
|
_pendingAttachments.splice(i, 1);
|
||||||
renderPendingAttachments();
|
renderPendingAttachments();
|
||||||
@@ -184,23 +200,23 @@ function getMessageText(el) {
|
|||||||
|
|
||||||
function createMessageActions(role) {
|
function createMessageActions(role) {
|
||||||
const bar = document.createElement('div');
|
const bar = document.createElement('div');
|
||||||
bar.className = 'message-actions';
|
bar.className = 'flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity';
|
||||||
|
|
||||||
// Copy button — all messages
|
// Copy button — all messages
|
||||||
const copyBtn = document.createElement('button');
|
const copyBtn = document.createElement('button');
|
||||||
copyBtn.className = 'msg-action-btn';
|
copyBtn.className = 'inline-flex items-center justify-center w-6 h-6 rounded text-zinc-500 hover:text-zinc-50 hover:bg-zinc-800 transition-colors';
|
||||||
copyBtn.title = 'Copy';
|
copyBtn.title = 'Copy';
|
||||||
copyBtn.innerHTML = COPY_ICON;
|
copyBtn.innerHTML = COPY_ICON;
|
||||||
copyBtn.addEventListener('click', () => {
|
copyBtn.addEventListener('click', () => {
|
||||||
const msg = bar.closest('.message');
|
const msgEl = bar.previousElementSibling;
|
||||||
if (!msg) {return;}
|
if (!msgEl) {return;}
|
||||||
const text = getMessageText(msg);
|
const text = getMessageText(msgEl);
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
copyBtn.innerHTML = CHECK_ICON;
|
copyBtn.innerHTML = CHECK_ICON;
|
||||||
copyBtn.classList.add('copied');
|
copyBtn.classList.add('text-green-500');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copyBtn.innerHTML = COPY_ICON;
|
copyBtn.innerHTML = COPY_ICON;
|
||||||
copyBtn.classList.remove('copied');
|
copyBtn.classList.remove('text-green-500');
|
||||||
}, 1500);
|
}, 1500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -209,13 +225,13 @@ function createMessageActions(role) {
|
|||||||
// Edit button — user messages only
|
// Edit button — user messages only
|
||||||
if (role === 'user') {
|
if (role === 'user') {
|
||||||
const editBtn = document.createElement('button');
|
const editBtn = document.createElement('button');
|
||||||
editBtn.className = 'msg-action-btn';
|
editBtn.className = 'inline-flex items-center justify-center w-6 h-6 rounded text-zinc-500 hover:text-zinc-50 hover:bg-zinc-800 transition-colors';
|
||||||
editBtn.title = 'Edit';
|
editBtn.title = 'Edit';
|
||||||
editBtn.innerHTML = EDIT_ICON;
|
editBtn.innerHTML = EDIT_ICON;
|
||||||
editBtn.addEventListener('click', () => {
|
editBtn.addEventListener('click', () => {
|
||||||
const msg = bar.closest('.message');
|
const msgEl = bar.previousElementSibling;
|
||||||
if (!msg) {return;}
|
if (!msgEl) {return;}
|
||||||
const text = getMessageText(msg);
|
const text = getMessageText(msgEl);
|
||||||
const input = _elements.input;
|
const input = _elements.input;
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = text;
|
input.value = text;
|
||||||
@@ -268,8 +284,8 @@ function showSlashPopup(filtered) {
|
|||||||
|
|
||||||
for (let i = 0; i < filtered.length; i++) {
|
for (let i = 0; i < filtered.length; i++) {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'slash-popup-item' + (i === _slashPopupIndex ? ' selected' : '');
|
item.className = 'flex items-baseline gap-3 px-3 py-2 cursor-pointer transition-colors' + (i === _slashPopupIndex ? ' bg-zinc-800' : '');
|
||||||
item.innerHTML = `<span class="cmd-name">${escapeHtml(filtered[i].name)}</span><span class="cmd-desc">${escapeHtml(filtered[i].desc)}</span>`;
|
item.innerHTML = `<span class="text-blue-500 font-semibold text-sm min-w-[80px]">${escapeHtml(filtered[i].name)}</span><span class="text-zinc-500 text-xs">${escapeHtml(filtered[i].desc)}</span>`;
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
selectSlashCommand(filtered[i].name);
|
selectSlashCommand(filtered[i].name);
|
||||||
});
|
});
|
||||||
@@ -292,10 +308,14 @@ function hideSlashPopup() {
|
|||||||
function updatePopupSelection(_filtered) {
|
function updatePopupSelection(_filtered) {
|
||||||
const popup = _elements.slashPopup;
|
const popup = _elements.slashPopup;
|
||||||
if (!popup) {return;}
|
if (!popup) {return;}
|
||||||
const items = popup.querySelectorAll('.slash-popup-item');
|
const items = popup.children;
|
||||||
items.forEach((el, i) => {
|
for (let i = 0; i < items.length; i++) {
|
||||||
el.classList.toggle('selected', i === _slashPopupIndex);
|
if (i === _slashPopupIndex) {
|
||||||
});
|
items[i].classList.add('bg-zinc-800');
|
||||||
|
} else {
|
||||||
|
items[i].classList.remove('bg-zinc-800');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSlashCommand(name) {
|
function selectSlashCommand(name) {
|
||||||
@@ -515,7 +535,7 @@ async function loadHistory(client) {
|
|||||||
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
} catch {
|
} catch {
|
||||||
msgs.innerHTML = '<div class="empty-state">Could not load history</div>';
|
msgs.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">Could not load history</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,8 +579,8 @@ async function sendMessage(client, overrideText) {
|
|||||||
|
|
||||||
// Create placeholder for assistant response
|
// Create placeholder for assistant response
|
||||||
const placeholder = document.createElement('div');
|
const placeholder = document.createElement('div');
|
||||||
placeholder.className = 'message assistant streaming-cursor';
|
placeholder.className = 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50 streaming-cursor';
|
||||||
placeholder.innerHTML = '<span class="text-muted">Thinking...</span>';
|
placeholder.innerHTML = '<span class="text-zinc-500">Thinking...</span>';
|
||||||
_elements.messages.appendChild(placeholder);
|
_elements.messages.appendChild(placeholder);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|
||||||
@@ -575,17 +595,17 @@ async function sendMessage(client, overrideText) {
|
|||||||
|
|
||||||
stream.on('tool_end', (data) => {
|
stream.on('tool_end', (data) => {
|
||||||
// Replace the last tool_start spinner with completion marker
|
// Replace the last tool_start spinner with completion marker
|
||||||
const events = _elements.messages.querySelectorAll('.tool-event-group');
|
const events = _elements.messages.querySelectorAll('.border.border-zinc-800.rounded-md');
|
||||||
const last = events[events.length - 1];
|
const last = events[events.length - 1];
|
||||||
if (last) {
|
if (last) {
|
||||||
const header = last.querySelector('.tool-event-header');
|
const header = last.firstElementChild;
|
||||||
if (header && data.tool) {
|
if (header && data.tool) {
|
||||||
const icon = data.result?.success !== false ? '✓' : '✗';
|
const icon = data.result?.success !== false ? '✓' : '✗';
|
||||||
const cls = data.result?.success !== false ? 'status-ok' : 'status-error';
|
const cls = data.result?.success !== false ? 'text-green-500' : 'text-red-500';
|
||||||
header.innerHTML = `<span class="${cls}">${icon}</span> <strong>${escapeHtml(data.tool)}</strong>`;
|
header.innerHTML = `<span class="${cls}">${icon}</span> <strong>${escapeHtml(data.tool)}</strong>`;
|
||||||
}
|
}
|
||||||
// Add result body
|
// Add result body
|
||||||
const body = last.querySelector('.tool-event-body');
|
const body = last.lastElementChild;
|
||||||
if (body && data.result) {
|
if (body && data.result) {
|
||||||
body.textContent = data.result.output || data.result.error || '(no output)';
|
body.textContent = data.result.output || data.result.error || '(no output)';
|
||||||
}
|
}
|
||||||
@@ -595,7 +615,7 @@ async function sendMessage(client, overrideText) {
|
|||||||
|
|
||||||
stream.on('context_warning', (data) => {
|
stream.on('context_warning', (data) => {
|
||||||
const note = document.createElement('div');
|
const note = document.createElement('div');
|
||||||
note.className = 'message assistant';
|
note.className = 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-zinc-900 border border-zinc-800 text-zinc-50';
|
||||||
const text = data?.message || 'Context usage is getting high.';
|
const text = data?.message || 'Context usage is getting high.';
|
||||||
note.innerHTML = renderSafeMarkdown(`> ${text}`);
|
note.innerHTML = renderSafeMarkdown(`> ${text}`);
|
||||||
_elements.messages.insertBefore(note, placeholder);
|
_elements.messages.insertBefore(note, placeholder);
|
||||||
@@ -607,11 +627,18 @@ async function sendMessage(client, overrideText) {
|
|||||||
placeholder.classList.remove('streaming-cursor');
|
placeholder.classList.remove('streaming-cursor');
|
||||||
const content = done?.content ?? done?.text ?? '(no response)';
|
const content = done?.content ?? done?.text ?? '(no response)';
|
||||||
placeholder.innerHTML = renderSafeMarkdown(content);
|
placeholder.innerHTML = renderSafeMarkdown(content);
|
||||||
placeholder.appendChild(createMessageActions('assistant'));
|
|
||||||
|
// Wrap placeholder in a proper message wrapper for action buttons
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.className = 'flex flex-col gap-1.5 max-w-[85%] md:max-w-[75%] self-start group';
|
||||||
|
placeholder.parentNode.insertBefore(wrapper, placeholder);
|
||||||
|
wrapper.appendChild(placeholder);
|
||||||
|
wrapper.appendChild(createMessageActions('assistant'));
|
||||||
|
|
||||||
setTimeout(highlightCode, 0);
|
setTimeout(highlightCode, 0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
placeholder.classList.remove('streaming-cursor');
|
placeholder.classList.remove('streaming-cursor');
|
||||||
placeholder.className = 'message error';
|
placeholder.className = 'rounded-lg px-3.5 py-2.5 text-sm leading-relaxed break-words whitespace-pre-wrap bg-red-500/15 border border-red-500/30 text-zinc-50';
|
||||||
placeholder.textContent = `Error: ${err.message}`;
|
placeholder.textContent = `Error: ${err.message}`;
|
||||||
} finally {
|
} finally {
|
||||||
_sending = false;
|
_sending = false;
|
||||||
@@ -623,41 +650,41 @@ async function sendMessage(client, overrideText) {
|
|||||||
|
|
||||||
// ── Search SVG Icon ─────────────────────────────────────────
|
// ── Search SVG Icon ─────────────────────────────────────────
|
||||||
|
|
||||||
const SEARCH_ICON = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 3a5.5 5.5 0 0 1 4.38 8.82l4.15 4.15a.75.75 0 0 1-1.06 1.06l-4.15-4.15A5.5 5.5 0 1 1 8.5 3zm0 1.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="currentColor"/></svg>';
|
const SEARCH_ICON = '<svg class="w-3.5 h-3.5 fill-current shrink-0" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.5 3a5.5 5.5 0 0 1 4.38 8.82l4.15 4.15a.75.75 0 0 1-1.06 1.06l-4.15-4.15A5.5 5.5 0 1 1 8.5 3zm0 1.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z" fill="currentColor"/></svg>';
|
||||||
const ATTACH_ICON = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 4.75A3.75 3.75 0 0 1 11.25 1h.5A3.25 3.25 0 0 1 15 4.25V12a4.5 4.5 0 0 1-9 0V6.75a2.75 2.75 0 0 1 5.5 0V12a1.25 1.25 0 0 1-2.5 0V6.75a.75.75 0 0 0-1.5 0V12a2.75 2.75 0 0 0 5.5 0V6.75A4.25 4.25 0 0 0 5 6.75V12a6 6 0 0 0 12 0V4.25A4.75 4.75 0 0 0 11.75-.5h-.5A5.25 5.25 0 0 0 6 4.75v8a.75.75 0 0 0 1.5 0z" fill="currentColor"/></svg>';
|
const ATTACH_ICON = '<svg class="w-3.5 h-3.5 fill-current shrink-0" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 4.75A3.75 3.75 0 0 1 11.25 1h.5A3.25 3.25 0 0 1 15 4.25V12a4.5 4.5 0 0 1-9 0V6.75a2.75 2.75 0 0 1 5.5 0V12a1.25 1.25 0 0 1-2.5 0V6.75a.75.75 0 0 0-1.5 0V12a2.75 2.75 0 0 0 5.5 0V6.75A4.25 4.25 0 0 0 5 6.75V12a6 6 0 0 0 12 0V4.25A4.75 4.75 0 0 0 11.75-.5h-.5A5.25 5.25 0 0 0 6 4.75v8a.75.75 0 0 0 1.5 0z" fill="currentColor"/></svg>';
|
||||||
const COPY_ICON = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" fill="currentColor"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" fill="currentColor"/></svg>';
|
const COPY_ICON = '<svg class="w-3.5 h-3.5 fill-current" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z" fill="currentColor"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" fill="currentColor"/></svg>';
|
||||||
const CHECK_ICON = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>';
|
const CHECK_ICON = '<svg class="w-3.5 h-3.5 fill-current" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.75.75 0 0 1 1.06-1.06L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" fill="currentColor"/></svg>';
|
||||||
const EDIT_ICON = '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm1.414 1.06a.25.25 0 0 0-.354 0L3.463 11.1l-.47 1.64 1.64-.47 8.61-8.61a.25.25 0 0 0 0-.354Z" fill="currentColor"/></svg>';
|
const EDIT_ICON = '<svg class="w-3.5 h-3.5 fill-current" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm1.414 1.06a.25.25 0 0 0-.354 0L3.463 11.1l-.47 1.64 1.64-.47 8.61-8.61a.25.25 0 0 0 0-.354Z" fill="currentColor"/></svg>';
|
||||||
|
|
||||||
// ── Page Export ──────────────────────────────────────────────
|
// ── Page Export ──────────────────────────────────────────────
|
||||||
|
|
||||||
export const ChatPage = {
|
export const ChatPage = {
|
||||||
async render(el, client) {
|
async render(el, client) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="chat-layout">
|
<div class="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-3rem)] max-w-3xl">
|
||||||
<div class="chat-header">
|
<div class="flex items-center gap-3 pb-3 border-b border-zinc-800 mb-3 flex-wrap">
|
||||||
<select id="chat-session-select"></select>
|
<select id="chat-session-select" class="bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm outline-none focus:border-blue-500"></select>
|
||||||
<button id="chat-new-session" class="btn btn-secondary">+ New</button>
|
<button id="chat-new-session" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">+ New</button>
|
||||||
<button id="chat-load-history" class="btn btn-secondary">History</button>
|
<button id="chat-load-history" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">History</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-messages" id="chat-messages"></div>
|
<div class="flex-1 overflow-y-auto flex flex-col gap-3 py-3" id="chat-messages"></div>
|
||||||
<div class="chat-actions">
|
<div class="flex items-center gap-2 py-2 flex-wrap">
|
||||||
<button id="chat-search" class="btn-action" title="Search the web">
|
<button id="chat-search" class="btn-action inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-zinc-800 text-zinc-400 border border-zinc-700 hover:text-zinc-50 hover:border-zinc-600 transition-colors cursor-pointer select-none" title="Search the web">
|
||||||
${SEARCH_ICON}
|
${SEARCH_ICON}
|
||||||
<span class="btn-action-label">Search</span>
|
<span>Search</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="chat-attach" class="btn-action" title="Attach image">
|
<button id="chat-attach" class="btn-action inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium bg-zinc-800 text-zinc-400 border border-zinc-700 hover:text-zinc-50 hover:border-zinc-600 transition-colors cursor-pointer select-none" title="Attach image">
|
||||||
${ATTACH_ICON}
|
${ATTACH_ICON}
|
||||||
<span class="btn-action-label">Attach</span>
|
<span>Attach</span>
|
||||||
</button>
|
</button>
|
||||||
<input id="chat-file" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple class="hidden" />
|
<input id="chat-file" type="file" accept="image/png,image/jpeg,image/gif,image/webp" multiple class="hidden" />
|
||||||
<div id="chat-attachments" class="chat-attachments hidden"></div>
|
<div id="chat-attachments" class="inline-flex items-center gap-1.5 flex-wrap hidden"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-input-wrapper">
|
<div class="relative">
|
||||||
<div id="slash-popup" class="slash-popup hidden"></div>
|
<div id="slash-popup" class="absolute bottom-full left-0 right-0 mb-1 bg-zinc-900 border border-zinc-800 rounded-lg max-h-60 overflow-y-auto z-50 shadow-lg shadow-black/30 hidden"></div>
|
||||||
<div class="chat-input">
|
<div class="flex gap-2 pt-3 border-t border-zinc-800">
|
||||||
<textarea id="chat-input" placeholder="Type a message..." rows="1"></textarea>
|
<textarea id="chat-input" class="flex-1 bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm outline-none resize-none min-h-[42px] max-h-[150px] leading-relaxed focus:border-blue-500 transition-colors" placeholder="Type a message..." rows="1"></textarea>
|
||||||
<button id="chat-send" class="btn btn-primary">Send</button>
|
<button id="chat-send" class="px-4 py-2.5 bg-blue-500 text-zinc-950 font-semibold text-sm rounded-lg hover:opacity-85 disabled:opacity-40 disabled:cursor-not-allowed transition-opacity self-end">Send</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -691,7 +718,7 @@ export const ChatPage = {
|
|||||||
await loadSessions(client);
|
await loadSessions(client);
|
||||||
_elements.messages.innerHTML = '';
|
_elements.messages.innerHTML = '';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_elements.messages.innerHTML = `<div class="empty-state">Failed to create session: ${err.message}</div>`;
|
_elements.messages.innerHTML = `<div class="text-center py-12 px-6 text-zinc-500 text-sm">Failed to create session: ${err.message}</div>`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -795,14 +822,14 @@ export const ChatPage = {
|
|||||||
|
|
||||||
// Dismiss slash popup on outside click
|
// Dismiss slash popup on outside click
|
||||||
el.addEventListener('click', (e) => {
|
el.addEventListener('click', (e) => {
|
||||||
if (!e.target.closest('.chat-input-wrapper')) {
|
if (!e.target.closest('.relative')) {
|
||||||
hideSlashPopup();
|
hideSlashPopup();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there's a current session, show welcome
|
// If there's a current session, show welcome
|
||||||
if (!_currentSession) {
|
if (!_currentSession) {
|
||||||
_elements.messages.innerHTML = '<div class="empty-state">Select a session or create a new one to start chatting</div>';
|
_elements.messages.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">Select a session or create a new one to start chatting</div>';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+217
-204
@@ -126,46 +126,46 @@ function buildRollbackPatchesFromSnapshot(snapshot) {
|
|||||||
|
|
||||||
function renderSkeleton(el) {
|
function renderSkeleton(el) {
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<h1 class="page-title">Live Ops Dashboard</h1>
|
<h1 class="text-2xl font-semibold text-zinc-50 mb-6">Live Ops Dashboard</h1>
|
||||||
|
|
||||||
<h2 class="section-title">Core Counters</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Core Counters</h2>
|
||||||
<div class="stats-grid" id="ops-counters">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8" id="ops-counters">
|
||||||
<div class="stat-card"><div class="stat-label">Loading...</div><div class="stat-value">—</div></div>
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors"><div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Loading...</div><div class="text-2xl font-bold font-mono text-zinc-50">—</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Model Performance</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Model Performance</h2>
|
||||||
<div id="ops-model-table">
|
<div id="ops-model-table">
|
||||||
<div class="text-muted text-sm">Loading...</div>
|
<div class="text-sm text-zinc-500">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Session Analytics</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Session Analytics</h2>
|
||||||
<div id="ops-session-analytics">
|
<div id="ops-session-analytics">
|
||||||
<div class="text-muted text-sm">Loading...</div>
|
<div class="text-sm text-zinc-500">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Context Health</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Context Health</h2>
|
||||||
<div id="ops-context-health">
|
<div id="ops-context-health">
|
||||||
<div class="text-muted text-sm">Loading...</div>
|
<div class="text-sm text-zinc-500">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Assistant Health</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Assistant Health</h2>
|
||||||
<div id="ops-assistant-health">
|
<div id="ops-assistant-health">
|
||||||
<div class="text-muted text-sm">Loading...</div>
|
<div class="text-sm text-zinc-500">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Event Stream</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Event Stream</h2>
|
||||||
<div class="event-stream" id="ops-events">
|
<div class="max-h-72 overflow-y-auto bg-zinc-900 border border-zinc-800 rounded-lg p-2 font-mono text-xs" id="ops-events">
|
||||||
<div class="event-row event-level-info">Loading events...</div>
|
<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words text-zinc-400">Loading events...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Active Requests</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Active Requests</h2>
|
||||||
<div id="ops-requests">
|
<div id="ops-requests">
|
||||||
<div class="text-muted text-sm">Loading...</div>
|
<div class="text-sm text-zinc-500">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Services</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Services</h2>
|
||||||
<div id="ops-services" class="services-grid">
|
<div id="ops-services" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
<div class="text-muted text-sm">Loading...</div>
|
<div class="text-sm text-zinc-500">Loading...</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -185,13 +185,13 @@ function updateCounters(metrics, health) {
|
|||||||
{ label: 'Queue Depth', value: String(metrics?.queueDepth ?? 0), cls: '' },
|
{ label: 'Queue Depth', value: String(metrics?.queueDepth ?? 0), cls: '' },
|
||||||
{ label: 'Uptime', value: formatUptime(metrics?.uptime ?? 0), cls: '' },
|
{ label: 'Uptime', value: formatUptime(metrics?.uptime ?? 0), cls: '' },
|
||||||
{ label: 'Active Requests', value: String(metrics?.activeRequests ?? 0), cls: '' },
|
{ label: 'Active Requests', value: String(metrics?.activeRequests ?? 0), cls: '' },
|
||||||
{ label: 'Errors', value: String(errCount), cls: errCount > 0 ? 'error' : '' },
|
{ label: 'Errors', value: String(errCount), cls: errCount > 0 ? 'text-red-500' : '' },
|
||||||
];
|
];
|
||||||
|
|
||||||
el.innerHTML = cards.map(c =>
|
el.innerHTML = cards.map(c =>
|
||||||
`<div class="stat-card">
|
`<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">${c.label}</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">${c.label}</div>
|
||||||
<div class="stat-value ${c.cls}">${c.value}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50 ${c.cls}">${c.value}</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
).join('');
|
).join('');
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ function updateModelTable(metrics) {
|
|||||||
const calls = mc?.recentCalls ?? [];
|
const calls = mc?.recentCalls ?? [];
|
||||||
|
|
||||||
if (calls.length === 0) {
|
if (calls.length === 0) {
|
||||||
el.innerHTML = '<div class="text-muted text-sm">No model calls recorded yet</div>';
|
el.innerHTML = '<div class="text-sm text-zinc-500">No model calls recorded yet</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,41 +213,43 @@ function updateModelTable(metrics) {
|
|||||||
const errorRate = mc.errorRate ?? 0;
|
const errorRate = mc.errorRate ?? 0;
|
||||||
|
|
||||||
const summaryHtml = `
|
const summaryHtml = `
|
||||||
<div class="metrics-summary">
|
<div class="flex flex-wrap gap-4 md:gap-6 mb-3 text-sm text-zinc-400">
|
||||||
<div class="metric"><span>Total Calls:</span> <span class="metric-value">${totalCalls}</span></div>
|
<div><span>Total Calls:</span> <span class="font-mono text-zinc-50">${totalCalls}</span></div>
|
||||||
<div class="metric"><span>Avg Latency:</span> <span class="metric-value">${avgLatency}ms</span></div>
|
<div><span>Avg Latency:</span> <span class="font-mono text-zinc-50">${avgLatency}ms</span></div>
|
||||||
<div class="metric"><span>Error Rate:</span> <span class="metric-value">${(errorRate * 100).toFixed(2)}%</span></div>
|
<div><span>Error Rate:</span> <span class="font-mono text-zinc-50">${(errorRate * 100).toFixed(2)}%</span></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Show newest first
|
// Show newest first
|
||||||
const rows = [...calls].reverse().map(c => {
|
const rows = [...calls].reverse().map(c => {
|
||||||
const status = c.error ? '<span class="text-error">✗</span>' : '<span class="text-success">✓</span>';
|
const status = c.error ? '<span class="text-red-500">✗</span>' : '<span class="text-green-500">✓</span>';
|
||||||
return `<tr>
|
return `<tr class="hover:bg-zinc-800/50">
|
||||||
<td>${timeAgo(c.timestamp)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${timeAgo(c.timestamp)}</td>
|
||||||
<td>${escapeHtml(c.provider)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(c.provider)}</td>
|
||||||
<td>${c.latency}ms</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.latency}ms</td>
|
||||||
<td>${c.tokensPerSec.toFixed(1)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.tokensPerSec.toFixed(1)}</td>
|
||||||
<td>${c.inputTokens}/${c.outputTokens}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${c.inputTokens}/${c.outputTokens}</td>
|
||||||
<td>${status}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${status}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
${summaryHtml}
|
${summaryHtml}
|
||||||
<table>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Time</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Time</th>
|
||||||
<th>Provider</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Provider</th>
|
||||||
<th>Latency</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Latency</th>
|
||||||
<th>Tokens/sec</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Tokens/sec</th>
|
||||||
<th>In/Out</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">In/Out</th>
|
||||||
<th>Status</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>${rows}</tbody>
|
<tbody>${rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +260,7 @@ function updateEvents(eventsData) {
|
|||||||
const events = eventsData?.events ?? [];
|
const events = eventsData?.events ?? [];
|
||||||
|
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
el.innerHTML = '<div class="event-row event-level-info">No events recorded yet</div>';
|
el.innerHTML = '<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words text-zinc-400">No events recorded yet</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,8 +270,8 @@ function updateEvents(eventsData) {
|
|||||||
el.innerHTML = reversed.map(e => {
|
el.innerHTML = reversed.map(e => {
|
||||||
const time = formatTime(e.timestamp);
|
const time = formatTime(e.timestamp);
|
||||||
const level = (e.level || 'info').toUpperCase();
|
const level = (e.level || 'info').toUpperCase();
|
||||||
const cls = `event-level-${e.level || 'info'}`;
|
const levelColor = e.level === 'error' ? 'text-red-500' : e.level === 'warn' ? 'text-amber-500' : 'text-zinc-400';
|
||||||
return `<div class="event-row ${cls}">[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}</div>`;
|
return `<div class="px-2 py-1 border-b border-zinc-800/50 last:border-0 break-words ${levelColor}">[${time}] [${level}] ${escapeHtml(e.source)}: ${escapeHtml(e.message)}</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Auto-scroll to bottom
|
// Auto-scroll to bottom
|
||||||
@@ -283,7 +285,7 @@ function updateActiveRequests(requestsData) {
|
|||||||
const requests = requestsData?.requests ?? [];
|
const requests = requestsData?.requests ?? [];
|
||||||
|
|
||||||
if (requests.length === 0) {
|
if (requests.length === 0) {
|
||||||
el.innerHTML = '<div class="text-muted text-sm">No active requests</div>';
|
el.innerHTML = '<div class="text-sm text-zinc-500">No active requests</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,26 +294,28 @@ function updateActiveRequests(requestsData) {
|
|||||||
? `${r.durationMs}ms`
|
? `${r.durationMs}ms`
|
||||||
: `${(r.durationMs / 1000).toFixed(1)}s`;
|
: `${(r.durationMs / 1000).toFixed(1)}s`;
|
||||||
const started = formatTime(r.startedAt);
|
const started = formatTime(r.startedAt);
|
||||||
return `<tr>
|
return `<tr class="hover:bg-zinc-800/50">
|
||||||
<td>${escapeHtml(r.sessionId)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(r.sessionId)}</td>
|
||||||
<td>${escapeHtml(r.channel)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(r.channel)}</td>
|
||||||
<td>${duration}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${duration}</td>
|
||||||
<td>${started}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${started}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<table>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
|
||||||
<th>Channel</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Channel</th>
|
||||||
<th>Duration</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Duration</th>
|
||||||
<th>Started</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Started</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>${rows}</tbody>
|
<tbody>${rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,95 +333,95 @@ function updateSessionAnalytics(analyticsData) {
|
|||||||
const avgMessagesPerSession = analyticsData?.averageMessagesPerSession ?? 0;
|
const avgMessagesPerSession = analyticsData?.averageMessagesPerSession ?? 0;
|
||||||
|
|
||||||
const summaryHtml = `
|
const summaryHtml = `
|
||||||
<div class="stats-grid">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Sessions (Window)</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Sessions (Window)</div>
|
||||||
<div class="stat-value">${formatNumber(totalSessions)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalSessions)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Messages (Window)</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Messages (Window)</div>
|
||||||
<div class="stat-value">${formatNumber(totalMessages)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalMessages)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Avg Session</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Avg Session</div>
|
||||||
<div class="stat-value">${formatSessionDurationFromMessages(avgMessagesPerSession)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatSessionDurationFromMessages(avgMessagesPerSession)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const topToolsHtml = topTools.length > 0
|
const topToolsHtml = topTools.length > 0
|
||||||
? `<ul class="tool-list">${topTools.map((tool) =>
|
? `<div>${topTools.map((tool) =>
|
||||||
`<li><span class="text-accent">${escapeHtml(tool.toolName)}</span> <span class="text-muted">(${formatNumber(tool.executions)})</span></li>`,
|
`<div class="py-1.5 border-b border-zinc-800/50 last:border-0 text-sm"><span class="text-blue-500">${escapeHtml(tool.toolName)}</span> <span class="text-zinc-500">(${formatNumber(tool.executions)})</span></div>`,
|
||||||
).join('')}</ul>`
|
).join('')}</div>`
|
||||||
: '<div class="text-muted text-sm">No tool usage captured in this window</div>';
|
: '<div class="text-sm text-zinc-500">No tool usage captured in this window</div>';
|
||||||
|
|
||||||
const topTopicsHtml = topTopics.length > 0
|
const topTopicsHtml = topTopics.length > 0
|
||||||
? `<ul class="tool-list">${topTopics.map((topic) =>
|
? `<div>${topTopics.map((topic) =>
|
||||||
`<li><span class="text-accent">${escapeHtml(topic.topic)}</span> <span class="text-muted">(${formatNumber(topic.occurrences)})</span></li>`,
|
`<div class="py-1.5 border-b border-zinc-800/50 last:border-0 text-sm"><span class="text-blue-500">${escapeHtml(topic.topic)}</span> <span class="text-zinc-500">(${formatNumber(topic.occurrences)})</span></div>`,
|
||||||
).join('')}</ul>`
|
).join('')}</div>`
|
||||||
: '<div class="text-muted text-sm">No indexed topics captured in this window</div>';
|
: '<div class="text-sm text-zinc-500">No indexed topics captured in this window</div>';
|
||||||
|
|
||||||
const topSessionsHtml = topSessions.length > 0
|
const topSessionsHtml = topSessions.length > 0
|
||||||
? `<table>
|
? `<div class="overflow-x-auto"><table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
|
||||||
<th>Messages</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
|
||||||
<th>Last Active</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Last Active</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${topSessions.map((session) => `
|
${topSessions.map((session) => `
|
||||||
<tr>
|
<tr class="hover:bg-zinc-800/50">
|
||||||
<td>${escapeHtml(session.sessionId)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(session.sessionId)}</td>
|
||||||
<td>${formatNumber(session.messages)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(session.messages)}</td>
|
||||||
<td>${timeAgo(session.lastActivity * 1000)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${timeAgo(session.lastActivity * 1000)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>`
|
</table></div>`
|
||||||
: '<div class="text-muted text-sm">No session activity in this window</div>';
|
: '<div class="text-sm text-zinc-500">No session activity in this window</div>';
|
||||||
|
|
||||||
const dailyHtml = daily.length > 0
|
const dailyHtml = daily.length > 0
|
||||||
? `<table>
|
? `<div class="overflow-x-auto"><table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Day</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Day</th>
|
||||||
<th>Sessions</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Sessions</th>
|
||||||
<th>Messages</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${daily.slice(0, 7).map((row) => `
|
${daily.slice(0, 7).map((row) => `
|
||||||
<tr>
|
<tr class="hover:bg-zinc-800/50">
|
||||||
<td>${formatDay(row.day)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatDay(row.day)}</td>
|
||||||
<td>${formatNumber(row.sessions)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(row.sessions)}</td>
|
||||||
<td>${formatNumber(row.messages)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(row.messages)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>`
|
</table></div>`
|
||||||
: '<div class="text-muted text-sm">No daily activity in this window</div>';
|
: '<div class="text-sm text-zinc-500">No daily activity in this window</div>';
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
${summaryHtml}
|
${summaryHtml}
|
||||||
<div class="services-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div class="service-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
|
||||||
<span class="service-name">Top Tools</span>
|
<div class="text-sm font-semibold text-zinc-50 mb-3">Top Tools</div>
|
||||||
${topToolsHtml}
|
${topToolsHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="service-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
|
||||||
<span class="service-name">Top Topics</span>
|
<div class="text-sm font-semibold text-zinc-50 mb-3">Top Topics</div>
|
||||||
${topTopicsHtml}
|
${topTopicsHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="services-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||||
<div class="service-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
|
||||||
<span class="service-name">Top Sessions</span>
|
<div class="text-sm font-semibold text-zinc-50 mb-3">Top Sessions</div>
|
||||||
${topSessionsHtml}
|
${topSessionsHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="service-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4">
|
||||||
<span class="service-name">Daily Trend (Last 7 Rows)</span>
|
<div class="text-sm font-semibold text-zinc-50 mb-3">Daily Trend (Last 7 Rows)</div>
|
||||||
${dailyHtml}
|
${dailyHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -430,7 +434,7 @@ function updateContextHealth(contextData) {
|
|||||||
|
|
||||||
const sessions = contextData?.sessions ?? [];
|
const sessions = contextData?.sessions ?? [];
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
el.innerHTML = '<div class="text-muted text-sm">No active context usage snapshots</div>';
|
el.innerHTML = '<div class="text-sm text-zinc-500">No active context usage snapshots</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,18 +444,18 @@ function updateContextHealth(contextData) {
|
|||||||
const overThreshold = sessions.filter(s => (s.budget?.shouldCompact ?? false)).length;
|
const overThreshold = sessions.filter(s => (s.budget?.shouldCompact ?? false)).length;
|
||||||
|
|
||||||
const summary = `
|
const summary = `
|
||||||
<div class="stats-grid">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Highest Usage</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Highest Usage</div>
|
||||||
<div class="stat-value ${highest >= 90 ? 'error' : ''}">${highest.toFixed(1)}%</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50 ${highest >= 90 ? 'text-red-500' : ''}">${highest.toFixed(1)}%</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Sessions Near Limit</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Sessions Near Limit</div>
|
||||||
<div class="stat-value ${overThreshold > 0 ? 'error' : ''}">${overThreshold}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50 ${overThreshold > 0 ? 'text-red-500' : ''}">${overThreshold}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Active Snapshots</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Active Snapshots</div>
|
||||||
<div class="stat-value">${sessions.length}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${sessions.length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -459,28 +463,30 @@ function updateContextHealth(contextData) {
|
|||||||
const rows = top.map((entry) => {
|
const rows = top.map((entry) => {
|
||||||
const budget = entry.budget ?? {};
|
const budget = entry.budget ?? {};
|
||||||
const usage = budget.usagePct ?? 0;
|
const usage = budget.usagePct ?? 0;
|
||||||
const cls = usage >= 95 ? 'text-error' : usage >= 85 ? 'status-warning' : '';
|
const cls = usage >= 95 ? 'text-red-500' : usage >= 85 ? 'text-amber-500' : '';
|
||||||
return `<tr>
|
return `<tr class="hover:bg-zinc-800/50">
|
||||||
<td>${escapeHtml(entry.sessionId)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${escapeHtml(entry.sessionId)}</td>
|
||||||
<td class="${cls}">${usage.toFixed(1)}%</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono ${cls}">${usage.toFixed(1)}%</td>
|
||||||
<td>${formatNumber(budget.estimatedTokens ?? 0)} / ${formatNumber(budget.contextWindow ?? 0)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50 font-mono">${formatNumber(budget.estimatedTokens ?? 0)} / ${formatNumber(budget.contextWindow ?? 0)}</td>
|
||||||
<td>${budget.shouldCompact ? 'yes' : 'no'}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${budget.shouldCompact ? 'yes' : 'no'}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
${summary}
|
${summary}
|
||||||
<table>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
|
||||||
<th>Usage</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Usage</th>
|
||||||
<th>Estimated Tokens</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Estimated Tokens</th>
|
||||||
<th>Should Compact</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Should Compact</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>${rows}</tbody>
|
<tbody>${rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +494,7 @@ async function applyAssistantPatch(patches, statusEl) {
|
|||||||
if (!_dashboardClient) {return;}
|
if (!_dashboardClient) {return;}
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = 'Saving...';
|
statusEl.textContent = 'Saving...';
|
||||||
statusEl.className = 'text-sm text-muted';
|
statusEl.className = 'text-sm text-zinc-500';
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await _dashboardClient.call('config.patch', { patches });
|
const result = await _dashboardClient.call('config.patch', { patches });
|
||||||
@@ -500,22 +506,22 @@ async function applyAssistantPatch(patches, statusEl) {
|
|||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
if (persistError) {
|
if (persistError) {
|
||||||
statusEl.textContent = `Save failed: ${persistError}`;
|
statusEl.textContent = `Save failed: ${persistError}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
} else if (rejected.length > 0) {
|
} else if (rejected.length > 0) {
|
||||||
statusEl.textContent = `Rejected: ${rejected.join(', ')}`;
|
statusEl.textContent = `Rejected: ${rejected.join(', ')}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
} else if (!persisted) {
|
} else if (!persisted) {
|
||||||
statusEl.textContent = `Runtime saved (${applied.length} updated)`;
|
statusEl.textContent = `Runtime saved (${applied.length} updated)`;
|
||||||
statusEl.className = 'text-sm text-muted';
|
statusEl.className = 'text-sm text-zinc-500';
|
||||||
} else {
|
} else {
|
||||||
statusEl.textContent = `Saved (${applied.length} updated)`;
|
statusEl.textContent = `Saved (${applied.length} updated)`;
|
||||||
statusEl.className = 'text-sm text-success';
|
statusEl.className = 'text-sm text-green-500';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`;
|
statusEl.textContent = `Save error: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -524,7 +530,7 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
|
|||||||
if (!_dashboardClient) {return;}
|
if (!_dashboardClient) {return;}
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = 'Triggering test briefing...';
|
statusEl.textContent = 'Triggering test briefing...';
|
||||||
statusEl.className = 'text-sm text-muted';
|
statusEl.className = 'text-sm text-zinc-500';
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const result = await _dashboardClient.call('tools.invoke', {
|
const result = await _dashboardClient.call('tools.invoke', {
|
||||||
@@ -536,7 +542,7 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
|
|||||||
const output = typeof result.output === 'string' ? result.output : 'Triggered.';
|
const output = typeof result.output === 'string' ? result.output : 'Triggered.';
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = output;
|
statusEl.textContent = output;
|
||||||
statusEl.className = 'text-sm text-success';
|
statusEl.className = 'text-sm text-green-500';
|
||||||
}
|
}
|
||||||
_lastBriefingTestAt = Date.now();
|
_lastBriefingTestAt = Date.now();
|
||||||
return true;
|
return true;
|
||||||
@@ -544,13 +550,13 @@ async function triggerDailyBriefingTest(jobName, statusEl) {
|
|||||||
|
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = result?.error ?? 'Failed to trigger briefing.';
|
statusEl.textContent = result?.error ?? 'Failed to trigger briefing.';
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = `Trigger error: ${error instanceof Error ? error.message : String(error)}`;
|
statusEl.textContent = `Trigger error: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -592,103 +598,103 @@ function updateAssistantHealth(configData) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const chip = (label, value) => `
|
const chip = (label, value) => `
|
||||||
<div class="assistant-chip">
|
<div class="flex justify-between items-center px-3 py-2.5 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
|
||||||
<span class="assistant-chip-label">${escapeHtml(label)}</span>
|
<span class="text-zinc-400">${escapeHtml(label)}</span>
|
||||||
<span class="assistant-chip-value ${value ? 'text-success' : 'text-muted'}">${value ? 'ON' : 'OFF'}</span>
|
<span class="font-bold ${value ? 'text-green-500' : 'text-zinc-500'}">${value ? 'ON' : 'OFF'}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="assistant-health-grid">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-2 mb-4">
|
||||||
${chip('Announce Mode', announce)}
|
${chip('Announce Mode', announce)}
|
||||||
${chip('Daily Briefing', dailyBriefing)}
|
${chip('Daily Briefing', dailyBriefing)}
|
||||||
${chip('Memory Daily Log', memoryDaily)}
|
${chip('Memory Daily Log', memoryDaily)}
|
||||||
${chip('Proactive Extract', memoryProactive)}
|
${chip('Proactive Extract', memoryProactive)}
|
||||||
${chip('TTS Replies', ttsEnabled)}
|
${chip('TTS Replies', ttsEnabled)}
|
||||||
<div class="assistant-chip">
|
<div class="flex justify-between items-center px-3 py-2.5 bg-zinc-900 border border-zinc-800 rounded-lg text-sm">
|
||||||
<span class="assistant-chip-label">Extract Threshold</span>
|
<span class="text-zinc-400">Extract Threshold</span>
|
||||||
<span class="assistant-chip-value">${Number.isFinite(proactiveThreshold) ? proactiveThreshold : 1}</span>
|
<span class="font-bold">${Number.isFinite(proactiveThreshold) ? proactiveThreshold : 1}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-actions">
|
<div class="flex flex-wrap gap-2 mb-4">
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-announce">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-announce">
|
||||||
${announce ? 'Disable Announce Mode' : 'Enable Announce Mode'}
|
${announce ? 'Disable Announce Mode' : 'Enable Announce Mode'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-daily-briefing">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-daily-briefing">
|
||||||
${dailyBriefing ? 'Disable Daily Briefing' : 'Enable Daily Briefing'}
|
${dailyBriefing ? 'Disable Daily Briefing' : 'Enable Daily Briefing'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-memory-daily">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-memory-daily">
|
||||||
${memoryDaily ? 'Disable Daily Log' : 'Enable Daily Log'}
|
${memoryDaily ? 'Disable Daily Log' : 'Enable Daily Log'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-memory-proactive">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-memory-proactive">
|
||||||
${memoryProactive ? 'Disable Proactive Extract' : 'Enable Proactive Extract'}
|
${memoryProactive ? 'Disable Proactive Extract' : 'Enable Proactive Extract'}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="toggle-tts">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="toggle-tts">
|
||||||
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
|
${ttsEnabled ? 'Disable TTS' : 'Enable TTS'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-playbooks">
|
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
|
||||||
<div class="assistant-preview-title">Assistant Playbooks</div>
|
<div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Playbooks</div>
|
||||||
<div class="assistant-playbook-grid">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 my-3">
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-executive">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-executive">
|
||||||
Executive
|
Executive
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-operator">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-operator">
|
||||||
Operator
|
Operator
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-focus">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-focus">
|
||||||
Focus
|
Focus
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="playbook-undo" ${_lastPlaybookRollbackPatches ? '' : 'disabled'}>
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="playbook-undo" ${_lastPlaybookRollbackPatches ? '' : 'disabled'}>
|
||||||
Undo Last Playbook
|
Undo Last Playbook
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-muted">Executive: announce + voice + aggressive interrupt. Operator: announce + memory-first + steer backlog. Focus: reactive, quieter mode.</div>
|
<div class="text-sm text-zinc-500">Executive: announce + voice + aggressive interrupt. Operator: announce + memory-first + steer backlog. Focus: reactive, quieter mode.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-setup">
|
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
|
||||||
<div class="assistant-preview-title">Assistant Activation Checklist</div>
|
<div class="text-sm font-semibold text-zinc-50 mb-3">Assistant Activation Checklist</div>
|
||||||
<ul class="assistant-checklist">
|
<div class="space-y-1 mb-4">
|
||||||
${checklistRows.map((row) => `
|
${checklistRows.map((row) => `
|
||||||
<li class="${row.done ? 'done' : ''}">
|
<div class="flex items-center gap-2 py-1 text-sm ${row.done ? 'text-zinc-50' : 'text-zinc-500'}">
|
||||||
<span class="assistant-check">${row.done ? '✓' : '○'}</span>
|
<span class="w-5 text-center font-bold">${row.done ? '✓' : '○'}</span>
|
||||||
<span>${escapeHtml(row.label)}</span>
|
<span>${escapeHtml(row.label)}</span>
|
||||||
</li>
|
</div>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</ul>
|
</div>
|
||||||
<div class="assistant-setup-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
<label class="assistant-field">
|
<label class="flex flex-col gap-1.5">
|
||||||
<span>Briefing output channel</span>
|
<span class="text-sm text-zinc-400">Briefing output channel</span>
|
||||||
<input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" />
|
<input id="assist-brief-channel" type="text" value="${escapeHtml(briefingOutput?.channel ?? '')}" placeholder="telegram" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-field">
|
<label class="flex flex-col gap-1.5">
|
||||||
<span>Briefing output peer/chat id</span>
|
<span class="text-sm text-zinc-400">Briefing output peer/chat id</span>
|
||||||
<input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" />
|
<input id="assist-brief-peer" type="text" value="${escapeHtml(briefingOutput?.peer ?? '')}" placeholder="123456789" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-actions">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="save-briefing-output">
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="save-briefing-output">
|
||||||
Save Briefing Output
|
Save Briefing Output
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-preview">
|
<div class="mt-4 p-4 border border-zinc-800 rounded-lg bg-zinc-900">
|
||||||
<div class="assistant-preview-header">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<div class="assistant-preview-title">Morning Brief Preview</div>
|
<div class="text-sm font-semibold text-zinc-50">Morning Brief Preview</div>
|
||||||
<button class="btn btn-secondary assistant-action-btn" data-action="test-daily-briefing" ${briefingReady ? '' : 'disabled'}>
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors assistant-action-btn" data-action="test-daily-briefing" ${briefingReady ? '' : 'disabled'}>
|
||||||
Send Test Briefing
|
Send Test Briefing
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-preview-meta text-sm text-muted">
|
<div class="flex flex-wrap gap-3 text-sm text-zinc-500 mb-3">
|
||||||
<span>name: <code>${escapeHtml(briefingName)}</code></span>
|
<span>name: <code class="font-mono">${escapeHtml(briefingName)}</code></span>
|
||||||
<span>schedule: <code>${escapeHtml(briefingSchedule)}</code></span>
|
<span>schedule: <code class="font-mono">${escapeHtml(briefingSchedule)}</code></span>
|
||||||
<span>timezone: <code>${escapeHtml(briefingTimezone)}</code></span>
|
<span>timezone: <code class="font-mono">${escapeHtml(briefingTimezone)}</code></span>
|
||||||
<span>tier: <code>${escapeHtml(briefingModelTier)}</code></span>
|
<span>tier: <code class="font-mono">${escapeHtml(briefingModelTier)}</code></span>
|
||||||
<span>output: <code>${escapeHtml(briefingOutputLabel)}</code></span>
|
<span>output: <code class="font-mono">${escapeHtml(briefingOutputLabel)}</code></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-preview-body"><code>${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}</code></div>
|
<div class="max-h-44 overflow-y-auto bg-zinc-950 border border-zinc-800 rounded-md p-3"><code class="text-sm text-zinc-400 font-mono whitespace-pre-wrap">${escapeHtml(briefingPrompt || 'No daily briefing prompt configured.')}</code></div>
|
||||||
${briefingReady ? '' : '<div class="text-sm text-muted">Enable daily briefing and set output channel/peer to test-send.</div>'}
|
${briefingReady ? '' : '<div class="text-sm text-zinc-500 mt-2">Enable daily briefing and set output channel/peer to test-send.</div>'}
|
||||||
</div>
|
</div>
|
||||||
<div id="ops-assistant-status" class="text-sm text-muted"></div>
|
<div id="ops-assistant-status" class="text-sm text-zinc-500 mt-4"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const statusEl = el.querySelector('#ops-assistant-status');
|
const statusEl = el.querySelector('#ops-assistant-status');
|
||||||
@@ -722,7 +728,7 @@ function updateAssistantHealth(configData) {
|
|||||||
if (!_lastPlaybookRollbackPatches) {
|
if (!_lastPlaybookRollbackPatches) {
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = 'No playbook changes to undo.';
|
statusEl.textContent = 'No playbook changes to undo.';
|
||||||
statusEl.className = 'text-sm text-muted';
|
statusEl.className = 'text-sm text-zinc-500';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -734,7 +740,7 @@ function updateAssistantHealth(configData) {
|
|||||||
if (!channel || !peer) {
|
if (!channel || !peer) {
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.textContent = 'Briefing output channel and peer are required.';
|
statusEl.textContent = 'Briefing output channel and peer are required.';
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -765,7 +771,7 @@ function _updateChannels(channelsData) {
|
|||||||
const channels = channelsData?.channels ?? [];
|
const channels = channelsData?.channels ?? [];
|
||||||
|
|
||||||
if (channels.length === 0) {
|
if (channels.length === 0) {
|
||||||
el.innerHTML = '<div class="text-muted text-sm">No channels registered</div>';
|
el.innerHTML = '<div class="text-sm text-zinc-500">No channels registered</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,27 +790,34 @@ function updateServices(servicesData) {
|
|||||||
const services = servicesData?.services ?? [];
|
const services = servicesData?.services ?? [];
|
||||||
|
|
||||||
if (services.length === 0) {
|
if (services.length === 0) {
|
||||||
el.innerHTML = '<div class="text-muted text-sm">No services configured</div>';
|
el.innerHTML = '<div class="text-sm text-zinc-500">No services configured</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.innerHTML = services.map(svc => {
|
el.innerHTML = services.map(svc => {
|
||||||
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
|
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
|
||||||
const statusClass = svc.status === 'connected'
|
const borderColor = svc.status === 'connected'
|
||||||
? 'connected'
|
? 'border-l-green-500'
|
||||||
: svc.status === 'configured'
|
: svc.status === 'configured'
|
||||||
? 'configured'
|
? 'border-l-blue-500'
|
||||||
: svc.status === 'error'
|
: svc.status === 'error'
|
||||||
? 'error'
|
? 'border-l-red-500'
|
||||||
: svc.status === 'not_configured'
|
: 'border-l-zinc-600 opacity-60';
|
||||||
? 'not-configured'
|
const statusColor = svc.status === 'connected'
|
||||||
: 'disconnected';
|
? 'text-green-500'
|
||||||
|
: svc.status === 'configured'
|
||||||
|
? 'text-blue-500'
|
||||||
|
: svc.status === 'error'
|
||||||
|
? 'text-red-500'
|
||||||
|
: 'text-zinc-500';
|
||||||
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
|
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
|
||||||
return `<div class="service-card service-${statusClass}">
|
return `<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-1 border-l-4 ${borderColor}">
|
||||||
<span class="service-type-icon">${typeIcon}</span>
|
<div class="flex items-center gap-2">
|
||||||
<span class="service-name">${escapeHtml(svc.name)}${itemCount}</span>
|
<span class="text-sm shrink-0">${typeIcon}</span>
|
||||||
<span class="service-status">${escapeHtml(svc.status)}</span>
|
<span class="text-sm font-semibold text-zinc-50">${escapeHtml(svc.name)}${itemCount}</span>
|
||||||
<span class="service-description text-muted text-xs">${escapeHtml(svc.description)}</span>
|
</div>
|
||||||
|
<span class="text-xs uppercase ${statusColor}">${escapeHtml(svc.status)}</span>
|
||||||
|
<span class="text-xs text-zinc-500">${escapeHtml(svc.description)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,21 +53,22 @@ async function loadSessionList() {
|
|||||||
const sessions = result.sessions ?? [];
|
const sessions = result.sessions ?? [];
|
||||||
|
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
listContainer.innerHTML = '<div class="empty-state">No sessions found</div>';
|
listContainer.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">No sessions found</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<table>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session ID</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session ID</th>
|
||||||
<th>Frontend</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Frontend</th>
|
||||||
<th>Messages</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Messages</th>
|
||||||
<th>Model</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Model</th>
|
||||||
<th>Queue</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Queue</th>
|
||||||
<th>Last Activity</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Last Activity</th>
|
||||||
<th>Actions</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -75,22 +76,24 @@ async function loadSessionList() {
|
|||||||
|
|
||||||
for (const s of sessions) {
|
for (const s of sessions) {
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr class="hover:bg-zinc-800/50">
|
||||||
<td><a href="#" class="session-view-link" data-id="${escapeHtml(s.id)}">${escapeHtml(s.id)}</a></td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50"><a href="#" class="text-blue-500 hover:underline session-view-link" data-id="${escapeHtml(s.id)}">${escapeHtml(s.id)}</a></td>
|
||||||
<td>${escapeHtml(s.frontend ?? (String(s.id).split(':')[0] || 'unknown'))}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(s.frontend ?? (String(s.id).split(':')[0] || 'unknown'))}</td>
|
||||||
<td>${s.messageCount ?? 0}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${s.messageCount ?? 0}</td>
|
||||||
<td>${escapeHtml(s.config?.modelTier ?? 'default')}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(s.config?.modelTier ?? 'default')}</td>
|
||||||
<td>${escapeHtml(formatQueue(s.config))}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(formatQueue(s.config))}</td>
|
||||||
<td>${escapeHtml(formatTime(s.lastMessageAt))}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${escapeHtml(formatTime(s.lastMessageAt))}</td>
|
||||||
<td class="session-actions">
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">
|
||||||
<button class="btn btn-secondary session-view-btn" data-id="${escapeHtml(s.id)}">View</button>
|
<div class="flex gap-1.5">
|
||||||
<button class="btn btn-danger session-delete-btn" data-id="${escapeHtml(s.id)}">Delete</button>
|
<button class="px-2.5 py-1 text-xs font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors session-view-btn" data-id="${escapeHtml(s.id)}">View</button>
|
||||||
|
<button class="px-2.5 py-1 text-xs font-medium rounded-md bg-red-500/15 text-red-500 border border-red-500/30 hover:bg-red-500 hover:text-white transition-colors session-delete-btn" data-id="${escapeHtml(s.id)}">Delete</button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</tbody></table>';
|
html += '</tbody></table></div>';
|
||||||
listContainer.innerHTML = html;
|
listContainer.innerHTML = html;
|
||||||
|
|
||||||
// Bind view buttons
|
// Bind view buttons
|
||||||
@@ -108,7 +111,7 @@ async function loadSessionList() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
listContainer.innerHTML = `<div class="empty-state">Failed to load sessions: ${err.message}</div>`;
|
listContainer.innerHTML = `<div class="text-center py-12 px-6 text-zinc-500 text-sm">Failed to load sessions: ${err.message}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,35 +119,42 @@ async function viewSession(sessionId) {
|
|||||||
const detailContainer = _el.querySelector('#session-detail');
|
const detailContainer = _el.querySelector('#session-detail');
|
||||||
if (!detailContainer) {return;}
|
if (!detailContainer) {return;}
|
||||||
|
|
||||||
detailContainer.innerHTML = '<div class="empty-state"><span class="spinner"></span> Loading...</div>';
|
detailContainer.innerHTML = '<div class="text-center py-12 px-6 text-zinc-500 text-sm"><span class="spinner"></span> Loading...</div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await _client.call('sessions.history', { sessionId });
|
const result = await _client.call('sessions.history', { sessionId });
|
||||||
const messages = result.messages ?? [];
|
const messages = result.messages ?? [];
|
||||||
|
|
||||||
|
const roleClasses = {
|
||||||
|
user: 'rounded-lg px-3 py-2 text-sm bg-blue-500/15 border border-blue-500/25 text-zinc-50',
|
||||||
|
assistant: 'rounded-lg px-3 py-2 text-sm bg-zinc-900 border border-zinc-800 text-zinc-50',
|
||||||
|
system: 'rounded-lg px-3 py-2 text-sm bg-zinc-800 text-zinc-400 border border-zinc-700',
|
||||||
|
};
|
||||||
|
|
||||||
let html = `
|
let html = `
|
||||||
<div class="session-detail">
|
<div class="mt-6">
|
||||||
<div class="session-detail-header">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="section-title">${escapeHtml(sessionId)}</h2>
|
<h2 class="text-lg font-semibold text-zinc-50">${escapeHtml(sessionId)}</h2>
|
||||||
<span class="text-muted text-sm">${messages.length} messages</span>
|
<span class="text-sm text-zinc-500">${messages.length} messages</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-history">
|
<div class="flex flex-col gap-3 max-h-[60vh] overflow-y-auto p-3 bg-zinc-900 border border-zinc-800 rounded-lg">
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (messages.length === 0) {
|
if (messages.length === 0) {
|
||||||
html += '<div class="empty-state">No messages in this session</div>';
|
html += '<div class="text-center py-12 px-6 text-zinc-500 text-sm">No messages in this session</div>';
|
||||||
} else {
|
} else {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
const role = msg.role ?? 'system';
|
const role = msg.role ?? 'system';
|
||||||
const content = msg.content ?? msg.text ?? '';
|
const content = msg.content ?? msg.text ?? '';
|
||||||
html += `<div class="message ${escapeHtml(role)}">${escapeHtml(content)}</div>`;
|
const cls = roleClasses[role] ?? roleClasses.system;
|
||||||
|
html += `<div class="${cls}">${escapeHtml(content)}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html += '</div></div>';
|
html += '</div></div>';
|
||||||
detailContainer.innerHTML = html;
|
detailContainer.innerHTML = html;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
detailContainer.innerHTML = `<div class="empty-state">Failed to load session: ${err.message}</div>`;
|
detailContainer.innerHTML = `<div class="text-center py-12 px-6 text-zinc-500 text-sm">Failed to load session: ${err.message}</div>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,10 +177,10 @@ export const SessionsPage = {
|
|||||||
_el = el;
|
_el = el;
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<h1 class="page-title">Sessions</h1>
|
<h1 class="text-2xl font-semibold text-zinc-50 mb-6">Sessions</h1>
|
||||||
<div class="status-row" style="margin-bottom: 0.75rem; gap: 0.75rem; flex-wrap: wrap;">
|
<div class="flex items-center gap-3 mb-4 flex-wrap">
|
||||||
<label class="text-sm text-muted">Frontend
|
<label class="text-sm text-zinc-400 flex items-center gap-1.5">Frontend
|
||||||
<select id="sessions-frontend-filter" style="margin-left: 0.35rem;">
|
<select id="sessions-frontend-filter" class="bg-zinc-900 text-zinc-50 border border-zinc-800 rounded-lg px-3 py-1.5 text-sm outline-none focus:border-blue-500">
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
<option value="ws">ws</option>
|
<option value="ws">ws</option>
|
||||||
<option value="tui">tui</option>
|
<option value="tui">tui</option>
|
||||||
@@ -180,11 +190,11 @@ export const SessionsPage = {
|
|||||||
<option value="mattermost">mattermost</option>
|
<option value="mattermost">mattermost</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="text-sm text-muted">
|
<label class="text-sm text-zinc-400 flex items-center gap-2 cursor-pointer">
|
||||||
<input id="sessions-include-inactive" type="checkbox" checked />
|
<input id="sessions-include-inactive" type="checkbox" checked />
|
||||||
Include inactive/persisted
|
Include inactive/persisted
|
||||||
</label>
|
</label>
|
||||||
<button id="sessions-refresh-btn" class="btn btn-secondary">Refresh</button>
|
<button id="sessions-refresh-btn" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="sessions-list"></div>
|
<div id="sessions-list"></div>
|
||||||
<div id="session-detail"></div>
|
<div id="session-detail"></div>
|
||||||
|
|||||||
@@ -51,12 +51,12 @@ async function renderPushStatus() {
|
|||||||
try {
|
try {
|
||||||
const status = await getPushStatus();
|
const status = await getPushStatus();
|
||||||
statusEl.textContent = describePushStatus(status);
|
statusEl.textContent = describePushStatus(status);
|
||||||
statusEl.className = status.subscribed ? 'text-sm text-success' : 'text-sm text-muted';
|
statusEl.className = status.subscribed ? 'text-sm text-green-500' : 'text-sm text-zinc-500';
|
||||||
enableBtn.disabled = !status.supported || !status.enabled || !status.configured || status.subscribed;
|
enableBtn.disabled = !status.supported || !status.enabled || !status.configured || status.subscribed;
|
||||||
disableBtn.disabled = !status.supported || !status.subscribed;
|
disableBtn.disabled = !status.supported || !status.subscribed;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = `Push status error: ${err.message}`;
|
statusEl.textContent = `Push status error: ${err.message}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
enableBtn.disabled = true;
|
enableBtn.disabled = true;
|
||||||
disableBtn.disabled = true;
|
disableBtn.disabled = true;
|
||||||
}
|
}
|
||||||
@@ -66,13 +66,13 @@ async function onEnablePush() {
|
|||||||
const statusEl = _el.querySelector('#push-status');
|
const statusEl = _el.querySelector('#push-status');
|
||||||
if (!statusEl) {return;}
|
if (!statusEl) {return;}
|
||||||
statusEl.textContent = 'Enabling push notifications...';
|
statusEl.textContent = 'Enabling push notifications...';
|
||||||
statusEl.className = 'text-sm text-muted';
|
statusEl.className = 'text-sm text-zinc-500';
|
||||||
try {
|
try {
|
||||||
await enablePushNotifications();
|
await enablePushNotifications();
|
||||||
await renderPushStatus();
|
await renderPushStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = `Enable failed: ${err.message}`;
|
statusEl.textContent = `Enable failed: ${err.message}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,13 +80,13 @@ async function onDisablePush() {
|
|||||||
const statusEl = _el.querySelector('#push-status');
|
const statusEl = _el.querySelector('#push-status');
|
||||||
if (!statusEl) {return;}
|
if (!statusEl) {return;}
|
||||||
statusEl.textContent = 'Disabling push notifications...';
|
statusEl.textContent = 'Disabling push notifications...';
|
||||||
statusEl.className = 'text-sm text-muted';
|
statusEl.className = 'text-sm text-zinc-500';
|
||||||
try {
|
try {
|
||||||
await disablePushNotifications();
|
await disablePushNotifications();
|
||||||
await renderPushStatus();
|
await renderPushStatus();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = `Disable failed: ${err.message}`;
|
statusEl.textContent = `Disable failed: ${err.message}`;
|
||||||
statusEl.className = 'text-sm text-error';
|
statusEl.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,8 +104,8 @@ async function loadSettings() {
|
|||||||
]);
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
_el.innerHTML = `
|
_el.innerHTML = `
|
||||||
<h1 class="page-title">Settings</h1>
|
<h1 class="text-2xl font-semibold text-zinc-50 mb-6">Settings</h1>
|
||||||
<div class="empty-state">Failed to load settings: ${err.message}</div>
|
<div class="text-center py-12 px-6 text-zinc-500 text-sm">Failed to load settings: ${err.message}</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -140,140 +140,147 @@ async function loadSettings() {
|
|||||||
const serviceList = services?.services ?? [];
|
const serviceList = services?.services ?? [];
|
||||||
|
|
||||||
_el.innerHTML = `
|
_el.innerHTML = `
|
||||||
<h1 class="page-title">Settings</h1>
|
<h1 class="text-2xl font-semibold text-zinc-50 mb-6">Settings</h1>
|
||||||
|
|
||||||
<h2 class="section-title">Personal Assistant Mode</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Personal Assistant Mode</h2>
|
||||||
<div class="settings-section">
|
<div class="mb-8">
|
||||||
<div class="assistant-mode-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||||||
<label class="assistant-toggle">
|
<label class="flex items-center gap-2.5 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400 cursor-pointer min-h-[44px]">
|
||||||
<input id="assist-delivery-announce" type="checkbox" ${deliveryMode === 'announce' ? 'checked' : ''} />
|
<input id="assist-delivery-announce" type="checkbox" ${deliveryMode === 'announce' ? 'checked' : ''} />
|
||||||
<span>Automation announce delivery mode</span>
|
<span>Automation announce delivery mode</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-toggle">
|
<label class="flex items-center gap-2.5 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400 cursor-pointer min-h-[44px]">
|
||||||
<input id="assist-daily-briefing" type="checkbox" ${dailyBriefingEnabled ? 'checked' : ''} />
|
<input id="assist-daily-briefing" type="checkbox" ${dailyBriefingEnabled ? 'checked' : ''} />
|
||||||
<span>Daily briefing enabled</span>
|
<span>Daily briefing enabled</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-toggle">
|
<label class="flex items-center gap-2.5 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400 cursor-pointer min-h-[44px]">
|
||||||
<input id="assist-memory-daily" type="checkbox" ${dailyMemoryEnabled ? 'checked' : ''} />
|
<input id="assist-memory-daily" type="checkbox" ${dailyMemoryEnabled ? 'checked' : ''} />
|
||||||
<span>Daily memory logging</span>
|
<span>Daily memory logging</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-toggle">
|
<label class="flex items-center gap-2.5 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400 cursor-pointer min-h-[44px]">
|
||||||
<input id="assist-memory-proactive" type="checkbox" ${proactiveExtractEnabled ? 'checked' : ''} />
|
<input id="assist-memory-proactive" type="checkbox" ${proactiveExtractEnabled ? 'checked' : ''} />
|
||||||
<span>Proactive memory extraction</span>
|
<span>Proactive memory extraction</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-field">
|
<label class="flex flex-col gap-2 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400">
|
||||||
<span>Proactive extract tool-call threshold</span>
|
<span>Proactive extract tool-call threshold</span>
|
||||||
<input id="assist-memory-min-tools" type="number" min="0" max="50" value="${Number.isFinite(proactiveMinToolCalls) ? proactiveMinToolCalls : 1}" />
|
<input id="assist-memory-min-tools" type="number" min="0" max="50" value="${Number.isFinite(proactiveMinToolCalls) ? proactiveMinToolCalls : 1}" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-toggle">
|
<label class="flex items-center gap-2.5 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400 cursor-pointer min-h-[44px]">
|
||||||
<input id="assist-tts-enabled" type="checkbox" ${ttsEnabled ? 'checked' : ''} />
|
<input id="assist-tts-enabled" type="checkbox" ${ttsEnabled ? 'checked' : ''} />
|
||||||
<span>TTS voice replies enabled</span>
|
<span>TTS voice replies enabled</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-field">
|
<label class="flex flex-col gap-2 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400">
|
||||||
<span>TTS channels (comma-separated, blank = all)</span>
|
<span>TTS channels (comma-separated, blank = all)</span>
|
||||||
<input id="assist-tts-channels" type="text" value="${escapeHtml(ttsChannelText)}" placeholder="telegram,discord,whatsapp" />
|
<input id="assist-tts-channels" type="text" value="${escapeHtml(ttsChannelText)}" placeholder="telegram,discord,whatsapp" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-field">
|
<label class="flex flex-col gap-2 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400">
|
||||||
<span>Briefing output channel</span>
|
<span>Briefing output channel</span>
|
||||||
<input id="assist-briefing-channel" type="text" value="${escapeHtml(briefingOutputChannel)}" placeholder="telegram" />
|
<input id="assist-briefing-channel" type="text" value="${escapeHtml(briefingOutputChannel)}" placeholder="telegram" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||||
</label>
|
</label>
|
||||||
<label class="assistant-field">
|
<label class="flex flex-col gap-2 bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2.5 text-sm text-zinc-400">
|
||||||
<span>Briefing output peer/chat id</span>
|
<span>Briefing output peer/chat id</span>
|
||||||
<input id="assist-briefing-peer" type="text" value="${escapeHtml(briefingOutputPeer)}" placeholder="123456789" />
|
<input id="assist-briefing-peer" type="text" value="${escapeHtml(briefingOutputPeer)}" placeholder="123456789" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm focus:border-blue-500 outline-none" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="assistant-actions">
|
<div class="flex items-center gap-3 mt-2">
|
||||||
<button id="assistant-mode-save" class="btn btn-primary">Save Assistant Mode</button>
|
<button id="assistant-mode-save" class="px-4 py-2 text-sm font-medium rounded-md bg-blue-500 text-zinc-950 hover:opacity-85 transition-opacity">Save Assistant Mode</button>
|
||||||
<span id="assistant-mode-status" class="text-sm text-muted"></span>
|
<span id="assistant-mode-status" class="text-sm text-zinc-500"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">WebChat Push Notifications</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">WebChat Push Notifications</h2>
|
||||||
<div class="settings-section">
|
<div class="mb-8">
|
||||||
${isPushSupported() ? '' : '<div class="text-sm text-muted">This browser does not support PushManager APIs.</div>'}
|
${isPushSupported() ? '' : '<div class="text-sm text-zinc-500 mb-2">This browser does not support PushManager APIs.</div>'}
|
||||||
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
<div class="flex gap-2 mb-2">
|
||||||
<button id="push-enable" class="btn btn-primary" type="button">Enable Push</button>
|
<button id="push-enable" class="px-4 py-2 text-sm font-medium rounded-md bg-blue-500 text-zinc-950 hover:opacity-85 transition-opacity" type="button">Enable Push</button>
|
||||||
<button id="push-disable" class="btn btn-secondary" type="button">Disable Push</button>
|
<button id="push-disable" class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors" type="button">Disable Push</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="push-status" class="text-sm text-muted"></div>
|
<div id="push-status" class="text-sm text-zinc-500"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Hook Patterns</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Hook Patterns</h2>
|
||||||
<div class="settings-section">
|
<div class="mb-8">
|
||||||
<div class="hook-editor">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="hook-group">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3">
|
||||||
<label>Confirm (require approval)</label>
|
<label class="block text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Confirm (require approval)</label>
|
||||||
<textarea id="hooks-confirm" rows="3">${escapeHtml(confirmPatterns.join('\n'))}</textarea>
|
<textarea id="hooks-confirm" rows="3" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm font-mono focus:border-blue-500 outline-none resize-y min-h-[60px]">${escapeHtml(confirmPatterns.join('\n'))}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="hook-group">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3">
|
||||||
<label>Log (allow + log)</label>
|
<label class="block text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Log (allow + log)</label>
|
||||||
<textarea id="hooks-log" rows="3">${escapeHtml(logPatterns.join('\n'))}</textarea>
|
<textarea id="hooks-log" rows="3" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm font-mono focus:border-blue-500 outline-none resize-y min-h-[60px]">${escapeHtml(logPatterns.join('\n'))}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="hook-group">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3">
|
||||||
<label>Silent (allow silently)</label>
|
<label class="block text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Silent (allow silently)</label>
|
||||||
<textarea id="hooks-silent" rows="3">${escapeHtml(silentPatterns.join('\n'))}</textarea>
|
<textarea id="hooks-silent" rows="3" class="w-full bg-zinc-950 text-zinc-50 border border-zinc-800 rounded-md px-3 py-2 text-sm font-mono focus:border-blue-500 outline-none resize-y min-h-[60px]">${escapeHtml(silentPatterns.join('\n'))}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="flex items-center gap-3 mt-2">
|
||||||
<button id="hooks-save" class="btn btn-primary">Save Hook Patterns</button>
|
<button id="hooks-save" class="px-4 py-2 text-sm font-medium rounded-md bg-blue-500 text-zinc-950 hover:opacity-85 transition-opacity">Save Hook Patterns</button>
|
||||||
<span id="hooks-status" class="text-sm text-muted" style="margin-left: 12px;"></span>
|
<span id="hooks-status" class="text-sm text-zinc-500"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Tools (${toolList.length})</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Tools (${toolList.length})</h2>
|
||||||
<div class="settings-section">
|
<div class="mb-8">
|
||||||
${toolList.length > 0 ? `
|
${toolList.length > 0 ? `
|
||||||
<table>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Name</th>
|
||||||
<th>Description</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Description</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${toolList.map(t => `
|
${toolList.map(t => `
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>${escapeHtml(t.name)}</code></td>
|
<td class="px-3 py-2 border-b border-zinc-800/50"><code class="font-mono text-blue-500 text-xs">${escapeHtml(t.name)}</code></td>
|
||||||
<td class="text-secondary">${escapeHtml(t.description ?? '')}</td>
|
<td class="px-3 py-2 border-b border-zinc-800/50 text-zinc-400">${escapeHtml(t.description ?? '')}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
` : '<div class="empty-state">No tools available</div>'}
|
</div>
|
||||||
|
` : '<div class="text-center py-12 px-6 text-zinc-500 text-sm">No tools available</div>'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Services</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Services</h2>
|
||||||
<div class="settings-section">
|
<div class="mb-8">
|
||||||
${serviceList.length > 0 ? `
|
${serviceList.length > 0 ? `
|
||||||
<div class="services-grid">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
${serviceList.map(svc => {
|
${serviceList.map(svc => {
|
||||||
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
|
const typeIcon = svc.type === 'channel' ? '📡' : svc.type === 'automation' ? '⚙️' : '🔧';
|
||||||
const statusClass = svc.status === 'connected'
|
const borderClass = svc.status === 'connected'
|
||||||
? 'connected'
|
? 'border-l-green-500'
|
||||||
: svc.status === 'configured'
|
: svc.status === 'configured'
|
||||||
? 'configured'
|
? 'border-l-blue-500'
|
||||||
: svc.status === 'error'
|
: svc.status === 'error'
|
||||||
? 'error'
|
? 'border-l-red-500'
|
||||||
: svc.status === 'not_configured'
|
: 'border-l-zinc-600 opacity-60';
|
||||||
? 'not-configured'
|
const statusColorClass = svc.status === 'connected'
|
||||||
: 'disconnected';
|
? 'text-green-500'
|
||||||
|
: svc.status === 'configured'
|
||||||
|
? 'text-blue-500'
|
||||||
|
: svc.status === 'error'
|
||||||
|
? 'text-red-500'
|
||||||
|
: 'text-zinc-500';
|
||||||
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
|
const itemCount = svc.itemCount ? ` (${svc.itemCount})` : '';
|
||||||
return `
|
return `
|
||||||
<div class="service-card service-${statusClass}">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-3 flex flex-col gap-1 border-l-4 ${borderClass}">
|
||||||
<span class="service-type-icon">${typeIcon}</span>
|
<span>${typeIcon}</span>
|
||||||
<span class="service-name">${escapeHtml(svc.name)}${itemCount}</span>
|
<span class="text-sm font-semibold text-zinc-50">${escapeHtml(svc.name)}${itemCount}</span>
|
||||||
<span class="service-status">${escapeHtml(svc.status)}</span>
|
<span class="text-xs uppercase ${statusColorClass}">${escapeHtml(svc.status)}</span>
|
||||||
<span class="service-description text-muted text-xs">${escapeHtml(svc.description ?? '')}</span>
|
<span class="text-xs text-zinc-500">${escapeHtml(svc.description ?? '')}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
</div>
|
</div>
|
||||||
` : '<div class="text-muted text-sm">No services found</div>'}
|
` : '<div class="text-sm text-zinc-500">No services found</div>'}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">Configuration (read-only)</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-8 pb-2 border-b border-zinc-800">Configuration (read-only)</h2>
|
||||||
<div class="settings-section">
|
<div class="mb-8">
|
||||||
<div class="config-view"><code>${escapeHtml(configJson)}</code></div>
|
<div class="bg-zinc-950 border border-zinc-800 rounded-lg p-4 max-h-96 overflow-y-auto font-mono text-xs text-zinc-400 whitespace-pre-wrap"><code>${escapeHtml(configJson)}</code></div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -288,7 +295,7 @@ async function loadSettings() {
|
|||||||
async function saveAssistantMode() {
|
async function saveAssistantMode() {
|
||||||
const status = _el.querySelector('#assistant-mode-status');
|
const status = _el.querySelector('#assistant-mode-status');
|
||||||
status.textContent = 'Saving...';
|
status.textContent = 'Saving...';
|
||||||
status.className = 'text-sm text-muted';
|
status.className = 'text-sm text-zinc-500';
|
||||||
|
|
||||||
const useAnnounce = Boolean(_el.querySelector('#assist-delivery-announce')?.checked);
|
const useAnnounce = Boolean(_el.querySelector('#assist-delivery-announce')?.checked);
|
||||||
const dailyBriefing = Boolean(_el.querySelector('#assist-daily-briefing')?.checked);
|
const dailyBriefing = Boolean(_el.querySelector('#assist-daily-briefing')?.checked);
|
||||||
@@ -330,23 +337,23 @@ async function saveAssistantMode() {
|
|||||||
|
|
||||||
if (rejected.length > 0) {
|
if (rejected.length > 0) {
|
||||||
status.textContent = `Partially saved. Rejected: ${rejected.join(', ')}`;
|
status.textContent = `Partially saved. Rejected: ${rejected.join(', ')}`;
|
||||||
status.className = 'text-sm text-error';
|
status.className = 'text-sm text-red-500';
|
||||||
} else if (persistError) {
|
} else if (persistError) {
|
||||||
status.textContent = `Save failed: ${persistError}`;
|
status.textContent = `Save failed: ${persistError}`;
|
||||||
status.className = 'text-sm text-error';
|
status.className = 'text-sm text-red-500';
|
||||||
} else if (!persisted) {
|
} else if (!persisted) {
|
||||||
status.textContent = `Saved in runtime only (${applied.length} updated)`;
|
status.textContent = `Saved in runtime only (${applied.length} updated)`;
|
||||||
status.className = 'text-sm text-muted';
|
status.className = 'text-sm text-zinc-500';
|
||||||
} else {
|
} else {
|
||||||
status.textContent = `Saved (${applied.length} updated)`;
|
status.textContent = `Saved (${applied.length} updated)`;
|
||||||
status.className = 'text-sm text-success';
|
status.className = 'text-sm text-green-500';
|
||||||
if (_settingsCache && _settingsCache.automation) {
|
if (_settingsCache && _settingsCache.automation) {
|
||||||
_settingsCache.automation.delivery_mode = useAnnounce ? 'announce' : 'shared_session';
|
_settingsCache.automation.delivery_mode = useAnnounce ? 'announce' : 'shared_session';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
status.textContent = `Error: ${err.message}`;
|
status.textContent = `Error: ${err.message}`;
|
||||||
status.className = 'text-sm text-error';
|
status.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -357,7 +364,7 @@ async function saveAssistantMode() {
|
|||||||
async function saveHooks() {
|
async function saveHooks() {
|
||||||
const status = _el.querySelector('#hooks-status');
|
const status = _el.querySelector('#hooks-status');
|
||||||
status.textContent = 'Saving...';
|
status.textContent = 'Saving...';
|
||||||
status.className = 'text-sm text-muted';
|
status.className = 'text-sm text-zinc-500';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const confirm = _el.querySelector('#hooks-confirm').value.split('\n').map(s => s.trim()).filter(Boolean);
|
const confirm = _el.querySelector('#hooks-confirm').value.split('\n').map(s => s.trim()).filter(Boolean);
|
||||||
@@ -379,20 +386,20 @@ async function saveHooks() {
|
|||||||
|
|
||||||
if (rejected.length > 0) {
|
if (rejected.length > 0) {
|
||||||
status.textContent = `Partially saved. Rejected: ${rejected.join(', ')}`;
|
status.textContent = `Partially saved. Rejected: ${rejected.join(', ')}`;
|
||||||
status.className = 'text-sm text-error';
|
status.className = 'text-sm text-red-500';
|
||||||
} else if (persistError) {
|
} else if (persistError) {
|
||||||
status.textContent = `Save failed: ${persistError}`;
|
status.textContent = `Save failed: ${persistError}`;
|
||||||
status.className = 'text-sm text-error';
|
status.className = 'text-sm text-red-500';
|
||||||
} else if (!persisted) {
|
} else if (!persisted) {
|
||||||
status.textContent = `Saved in runtime only (${applied.length} updated)`;
|
status.textContent = `Saved in runtime only (${applied.length} updated)`;
|
||||||
status.className = 'text-sm text-muted';
|
status.className = 'text-sm text-zinc-500';
|
||||||
} else {
|
} else {
|
||||||
status.textContent = `Saved (${applied.length} updated)`;
|
status.textContent = `Saved (${applied.length} updated)`;
|
||||||
status.className = 'text-sm text-success';
|
status.className = 'text-sm text-green-500';
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
status.textContent = `Error: ${err.message}`;
|
status.textContent = `Error: ${err.message}`;
|
||||||
status.className = 'text-sm text-error';
|
status.className = 'text-sm text-red-500';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear status after 5s
|
// Clear status after 5s
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ async function loadUsage(el, client) {
|
|||||||
data = await client.call('system.tokenUsage');
|
data = await client.call('system.tokenUsage');
|
||||||
contextData = await client.call('system.contextUsage');
|
contextData = await client.call('system.contextUsage');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
el.innerHTML = `<div class="empty-state">Failed to load usage: ${err.message}</div>`;
|
el.innerHTML = `<div class="text-center py-12 px-6 text-zinc-500 text-sm">Failed to load usage: ${err.message}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,30 +54,30 @@ async function loadUsage(el, client) {
|
|||||||
|
|
||||||
// Summary cards
|
// Summary cards
|
||||||
const summaryHtml = `
|
const summaryHtml = `
|
||||||
<div class="stats-grid">
|
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8">
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Total Input Tokens</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Total Input Tokens</div>
|
||||||
<div class="stat-value">${formatNumber(totalInput)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalInput)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Total Output Tokens</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Total Output Tokens</div>
|
||||||
<div class="stat-value">${formatNumber(totalOutput)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalOutput)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Total Tokens</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Total Tokens</div>
|
||||||
<div class="stat-value">${formatNumber(totalInput + totalOutput)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalInput + totalOutput)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">API Calls</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">API Calls</div>
|
||||||
<div class="stat-value">${formatNumber(totalCalls)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatNumber(totalCalls)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Estimated Cost</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Estimated Cost</div>
|
||||||
<div class="stat-value">${formatCost(totalCost)}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${formatCost(totalCost)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="bg-zinc-900 border border-zinc-800 rounded-lg p-4 hover:border-zinc-600 transition-colors">
|
||||||
<div class="stat-label">Active Sessions</div>
|
<div class="text-xs font-medium text-zinc-400 uppercase tracking-wide mb-2">Active Sessions</div>
|
||||||
<div class="stat-value">${sessions.length}</div>
|
<div class="text-2xl font-bold font-mono text-zinc-50">${sessions.length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -85,7 +85,7 @@ async function loadUsage(el, client) {
|
|||||||
// Per-session table
|
// Per-session table
|
||||||
let tableHtml = '';
|
let tableHtml = '';
|
||||||
if (sessions.length === 0) {
|
if (sessions.length === 0) {
|
||||||
tableHtml = '<div class="empty-state">No active sessions with usage data</div>';
|
tableHtml = '<div class="text-center py-12 px-6 text-zinc-500 text-sm">No active sessions with usage data</div>';
|
||||||
} else {
|
} else {
|
||||||
const rows = sessions.map(s => {
|
const rows = sessions.map(s => {
|
||||||
const inTok = s.total?.inputTokens ?? 0;
|
const inTok = s.total?.inputTokens ?? 0;
|
||||||
@@ -95,59 +95,61 @@ async function loadUsage(el, client) {
|
|||||||
const budget = contextBySession.get(s.sessionId);
|
const budget = contextBySession.get(s.sessionId);
|
||||||
const contextCell = budget
|
const contextCell = budget
|
||||||
? `${budget.usagePct.toFixed(1)}% (${formatNumber(budget.estimatedTokens)}/${formatNumber(budget.contextWindow)})`
|
? `${budget.usagePct.toFixed(1)}% (${formatNumber(budget.estimatedTokens)}/${formatNumber(budget.contextWindow)})`
|
||||||
: '<span class="text-muted">-</span>';
|
: '<span class="text-zinc-500">-</span>';
|
||||||
|
|
||||||
// Build delegation breakdown if present
|
// Build delegation breakdown if present
|
||||||
const delegationEntries = Object.entries(s.delegation ?? {});
|
const delegationEntries = Object.entries(s.delegation ?? {});
|
||||||
let delegationCell = '<span class="text-muted">-</span>';
|
let delegationCell = '<span class="text-zinc-500">-</span>';
|
||||||
if (delegationEntries.length > 0) {
|
if (delegationEntries.length > 0) {
|
||||||
delegationCell = delegationEntries.map(([tier, stats]) =>
|
delegationCell = delegationEntries.map(([tier, stats]) =>
|
||||||
`<span class="badge ok">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}`,
|
`<span class="inline-block px-2 py-0.5 text-xs rounded-full font-semibold bg-green-500/15 text-green-500">${tier}</span> ${formatNumber(stats.inputTokens)}/${formatNumber(stats.outputTokens)}`,
|
||||||
).join('<br>');
|
).join('<br>');
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr class="hover:bg-zinc-800/50">
|
||||||
<td title="${s.sessionId}">${truncateId(s.sessionId)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50" title="${s.sessionId}">${truncateId(s.sessionId)}</td>
|
||||||
<td>${formatNumber(inTok)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatNumber(inTok)}</td>
|
||||||
<td>${formatNumber(outTok)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatNumber(outTok)}</td>
|
||||||
<td>${formatNumber(inTok + outTok)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatNumber(inTok + outTok)}</td>
|
||||||
<td>${formatNumber(calls)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatNumber(calls)}</td>
|
||||||
<td>${formatCost(cost)}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${formatCost(cost)}</td>
|
||||||
<td>${contextCell}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${contextCell}</td>
|
||||||
<td>${delegationCell}</td>
|
<td class="px-3 py-2 text-zinc-50 border-b border-zinc-800/50">${delegationCell}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
tableHtml = `
|
tableHtml = `
|
||||||
<table>
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Session</th>
|
||||||
<th>Input</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Input</th>
|
||||||
<th>Output</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Output</th>
|
||||||
<th>Total</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Total</th>
|
||||||
<th>Calls</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Calls</th>
|
||||||
<th>Cost</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Cost</th>
|
||||||
<th>Context</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Context</th>
|
||||||
<th>Delegation</th>
|
<th class="text-left px-3 py-2 text-xs font-medium text-zinc-400 uppercase tracking-wide border-b border-zinc-800">Delegation</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${rows}
|
${rows}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
el.innerHTML = `
|
el.innerHTML = `
|
||||||
<div class="usage-header">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="page-title">Token Usage</h1>
|
<h1 class="text-2xl font-semibold text-zinc-50">Token Usage</h1>
|
||||||
<button class="btn btn-secondary" id="usage-refresh-btn">Refresh</button>
|
<button class="px-3 py-1.5 text-sm font-medium rounded-md border border-zinc-700 bg-zinc-800 text-zinc-200 hover:bg-zinc-700 transition-colors shrink-0" id="usage-refresh-btn">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
${summaryHtml}
|
${summaryHtml}
|
||||||
<h2 class="section-title">Per-Session Breakdown</h2>
|
<h2 class="text-lg font-semibold text-zinc-50 mb-4 mt-2 pb-2 border-b border-zinc-800">Per-Session Breakdown</h2>
|
||||||
${tableHtml}
|
${tableHtml}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user