diff --git a/src/gateway/ui/pages/chat.js b/src/gateway/ui/pages/chat.js index 1d5453d..43aaa0c 100644 --- a/src/gateway/ui/pages/chat.js +++ b/src/gateway/ui/pages/chat.js @@ -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 = ` ${escapeHtml(data.tool)}`; } 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 = `${icon} ${escapeHtml(data.tool)}`; } @@ -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 = `${escapeHtml(filtered[i].name)}${escapeHtml(filtered[i].desc)}`; + item.className = 'flex items-baseline gap-3 px-3 py-2 cursor-pointer transition-colors' + (i === _slashPopupIndex ? ' bg-zinc-800' : ''); + item.innerHTML = `${escapeHtml(filtered[i].name)}${escapeHtml(filtered[i].desc)}`; 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 = '