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() {
|
||||
if (typeof hljs !== 'undefined') {
|
||||
document.querySelectorAll('.chat-messages pre code').forEach(block => {
|
||||
document.querySelectorAll('#chat-messages pre code').forEach(block => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
@@ -44,10 +44,26 @@ function highlightCode() {
|
||||
|
||||
function createMessageEl(role, content) {
|
||||
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');
|
||||
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') {
|
||||
div.innerHTML = renderSafeMarkdown(content);
|
||||
@@ -67,16 +83,16 @@ function createMessageEl(role, content) {
|
||||
|
||||
function createToolEventEl(event, data) {
|
||||
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');
|
||||
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') {
|
||||
header.innerHTML = `<span class="spinner"></span> <strong>${escapeHtml(data.tool)}</strong>`;
|
||||
} else if (event === 'tool_end') {
|
||||
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>`;
|
||||
}
|
||||
|
||||
@@ -85,7 +101,7 @@ function createToolEventEl(event, data) {
|
||||
});
|
||||
|
||||
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) {
|
||||
body.textContent = JSON.stringify(data.args, null, 2);
|
||||
@@ -144,17 +160,17 @@ function renderPendingAttachments() {
|
||||
for (let i = 0; i < _pendingAttachments.length; i++) {
|
||||
const att = _pendingAttachments[i];
|
||||
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');
|
||||
name.className = 'attachment-name';
|
||||
name.className = 'max-w-[220px] overflow-hidden text-ellipsis whitespace-nowrap';
|
||||
name.textContent = att.filename || 'attachment';
|
||||
|
||||
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.title = 'Remove attachment';
|
||||
rm.textContent = '×';
|
||||
rm.textContent = '\u00d7';
|
||||
rm.addEventListener('click', () => {
|
||||
_pendingAttachments.splice(i, 1);
|
||||
renderPendingAttachments();
|
||||
@@ -184,23 +200,23 @@ function getMessageText(el) {
|
||||
|
||||
function createMessageActions(role) {
|
||||
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
|
||||
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.innerHTML = COPY_ICON;
|
||||
copyBtn.addEventListener('click', () => {
|
||||
const msg = bar.closest('.message');
|
||||
if (!msg) {return;}
|
||||
const text = getMessageText(msg);
|
||||
const msgEl = bar.previousElementSibling;
|
||||
if (!msgEl) {return;}
|
||||
const text = getMessageText(msgEl);
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
copyBtn.innerHTML = CHECK_ICON;
|
||||
copyBtn.classList.add('copied');
|
||||
copyBtn.classList.add('text-green-500');
|
||||
setTimeout(() => {
|
||||
copyBtn.innerHTML = COPY_ICON;
|
||||
copyBtn.classList.remove('copied');
|
||||
copyBtn.classList.remove('text-green-500');
|
||||
}, 1500);
|
||||
});
|
||||
});
|
||||
@@ -209,13 +225,13 @@ function createMessageActions(role) {
|
||||
// Edit button — user messages only
|
||||
if (role === 'user') {
|
||||
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.innerHTML = EDIT_ICON;
|
||||
editBtn.addEventListener('click', () => {
|
||||
const msg = bar.closest('.message');
|
||||
if (!msg) {return;}
|
||||
const text = getMessageText(msg);
|
||||
const msgEl = bar.previousElementSibling;
|
||||
if (!msgEl) {return;}
|
||||
const text = getMessageText(msgEl);
|
||||
const input = _elements.input;
|
||||
if (input) {
|
||||
input.value = text;
|
||||
@@ -268,8 +284,8 @@ function showSlashPopup(filtered) {
|
||||
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'slash-popup-item' + (i === _slashPopupIndex ? ' selected' : '');
|
||||
item.innerHTML = `<span class="cmd-name">${escapeHtml(filtered[i].name)}</span><span class="cmd-desc">${escapeHtml(filtered[i].desc)}</span>`;
|
||||
item.className = 'flex items-baseline gap-3 px-3 py-2 cursor-pointer transition-colors' + (i === _slashPopupIndex ? ' bg-zinc-800' : '');
|
||||
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', () => {
|
||||
selectSlashCommand(filtered[i].name);
|
||||
});
|
||||
@@ -292,10 +308,14 @@ function hideSlashPopup() {
|
||||
function updatePopupSelection(_filtered) {
|
||||
const popup = _elements.slashPopup;
|
||||
if (!popup) {return;}
|
||||
const items = popup.querySelectorAll('.slash-popup-item');
|
||||
items.forEach((el, i) => {
|
||||
el.classList.toggle('selected', i === _slashPopupIndex);
|
||||
});
|
||||
const items = popup.children;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (i === _slashPopupIndex) {
|
||||
items[i].classList.add('bg-zinc-800');
|
||||
} else {
|
||||
items[i].classList.remove('bg-zinc-800');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectSlashCommand(name) {
|
||||
@@ -515,7 +535,7 @@ async function loadHistory(client) {
|
||||
|
||||
scrollToBottom();
|
||||
} 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
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'message assistant streaming-cursor';
|
||||
placeholder.innerHTML = '<span class="text-muted">Thinking...</span>';
|
||||
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-zinc-500">Thinking...</span>';
|
||||
_elements.messages.appendChild(placeholder);
|
||||
scrollToBottom();
|
||||
|
||||
@@ -575,17 +595,17 @@ async function sendMessage(client, overrideText) {
|
||||
|
||||
stream.on('tool_end', (data) => {
|
||||
// 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];
|
||||
if (last) {
|
||||
const header = last.querySelector('.tool-event-header');
|
||||
const header = last.firstElementChild;
|
||||
if (header && data.tool) {
|
||||
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>`;
|
||||
}
|
||||
// Add result body
|
||||
const body = last.querySelector('.tool-event-body');
|
||||
const body = last.lastElementChild;
|
||||
if (body && data.result) {
|
||||
body.textContent = data.result.output || data.result.error || '(no output)';
|
||||
}
|
||||
@@ -595,7 +615,7 @@ async function sendMessage(client, overrideText) {
|
||||
|
||||
stream.on('context_warning', (data) => {
|
||||
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.';
|
||||
note.innerHTML = renderSafeMarkdown(`> ${text}`);
|
||||
_elements.messages.insertBefore(note, placeholder);
|
||||
@@ -607,11 +627,18 @@ async function sendMessage(client, overrideText) {
|
||||
placeholder.classList.remove('streaming-cursor');
|
||||
const content = done?.content ?? done?.text ?? '(no response)';
|
||||
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);
|
||||
} catch (err) {
|
||||
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}`;
|
||||
} finally {
|
||||
_sending = false;
|
||||
@@ -623,41 +650,41 @@ async function sendMessage(client, overrideText) {
|
||||
|
||||
// ── 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 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 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 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 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 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 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 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 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 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 ──────────────────────────────────────────────
|
||||
|
||||
export const ChatPage = {
|
||||
async render(el, client) {
|
||||
el.innerHTML = `
|
||||
<div class="chat-layout">
|
||||
<div class="chat-header">
|
||||
<select id="chat-session-select"></select>
|
||||
<button id="chat-new-session" class="btn btn-secondary">+ New</button>
|
||||
<button id="chat-load-history" class="btn btn-secondary">History</button>
|
||||
<div class="flex flex-col h-[calc(100vh-6rem)] md:h-[calc(100vh-3rem)] max-w-3xl">
|
||||
<div class="flex items-center gap-3 pb-3 border-b border-zinc-800 mb-3 flex-wrap">
|
||||
<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="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="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 class="chat-messages" id="chat-messages"></div>
|
||||
<div class="chat-actions">
|
||||
<button id="chat-search" class="btn-action" title="Search the web">
|
||||
<div class="flex-1 overflow-y-auto flex flex-col gap-3 py-3" id="chat-messages"></div>
|
||||
<div class="flex items-center gap-2 py-2 flex-wrap">
|
||||
<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}
|
||||
<span class="btn-action-label">Search</span>
|
||||
<span>Search</span>
|
||||
</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}
|
||||
<span class="btn-action-label">Attach</span>
|
||||
<span>Attach</span>
|
||||
</button>
|
||||
<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 class="chat-input-wrapper">
|
||||
<div id="slash-popup" class="slash-popup hidden"></div>
|
||||
<div class="chat-input">
|
||||
<textarea id="chat-input" placeholder="Type a message..." rows="1"></textarea>
|
||||
<button id="chat-send" class="btn btn-primary">Send</button>
|
||||
<div class="relative">
|
||||
<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="flex gap-2 pt-3 border-t border-zinc-800">
|
||||
<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="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>
|
||||
@@ -691,7 +718,7 @@ export const ChatPage = {
|
||||
await loadSessions(client);
|
||||
_elements.messages.innerHTML = '';
|
||||
} 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
|
||||
el.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.chat-input-wrapper')) {
|
||||
if (!e.target.closest('.relative')) {
|
||||
hideSlashPopup();
|
||||
}
|
||||
});
|
||||
|
||||
// If there's a current session, show welcome
|
||||
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>';
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user