diff --git a/templates/index.html b/templates/index.html index 69d588c..0b42a74 100644 --- a/templates/index.html +++ b/templates/index.html @@ -165,6 +165,14 @@ body { font-family: var(--font-body); background: var(--bg-primary); color: var( .ttr-badge.search { background:rgba(230,126,34,0.15); border:1px solid rgba(230,126,34,0.3); color:#e67e22; } .tps-badge { display:inline-block; padding:2px 8px; border-radius:10px; font-family:var(--font-mono); font-size:10px; margin-left:8px; background:rgba(72,181,224,0.15); border:1px solid rgba(72,181,224,0.3); color:var(--accent); } +.msg-toolbar { display:flex; gap:2px; margin-top:8px; opacity:0; transition:opacity 0.15s; align-items:center; } +.message .content:hover .msg-toolbar { opacity:1; } +.msg-toolbar button { background:none; border:none; color:var(--text-muted); cursor:pointer; padding:4px 6px; border-radius:4px; font-size:13px; line-height:1; display:flex; align-items:center; gap:4px; transition:color 0.15s,background 0.15s; } +.msg-toolbar button:hover { color:var(--text-primary); background:var(--bg-tertiary); } +.msg-toolbar button.active { color:var(--accent); } +.msg-toolbar .sep { width:1px; height:14px; background:var(--border); margin:0 4px; } +.msg-toolbar .toolbar-label { font-size:10px; font-family:var(--font-mono); color:var(--text-muted); margin-right:4px; } + .input-area { padding:16px 20px; border-top:1px solid var(--border); background:var(--bg-secondary); } .input-row-top { max-width:900px; margin:0 auto 8px; display:flex; gap:8px; align-items:center; } .input-row-top select { background:var(--bg-tertiary); border:1px solid var(--border); color:var(--text-secondary); font-family:var(--font-mono); font-size:11px; padding:4px 8px; border-radius:var(--radius); cursor:pointer; } @@ -1172,6 +1180,7 @@ async function sendSearch() { conversationHistory.push({ role: 'assistant', content: fullText }); updateTokenThermometer(); addCopyButtons(assistantDiv); + addMessageToolbar(assistantDiv); setStreamingState(false); await loadConversations(); } @@ -1350,6 +1359,7 @@ async function sendMessage() { conversationHistory.push({ role: 'assistant', content: fullText }); updateTokenThermometer(); addCopyButtons(assistantDiv); + addMessageToolbar(assistantDiv); setStreamingState(false); await loadConversations(); await loadMemoryStats(); @@ -1387,9 +1397,9 @@ function appendMessage(role, content, animate, isSearch = false) { const div = document.createElement('div'); div.className = 'message ' + role + (isSearch && role === 'assistant' ? ' search-result' : ''); if (!animate) div.style.animation = 'none'; - div.innerHTML = `
${role === 'user' ? 'YOU' : 'AI'}
${role}
${content ? renderMarkdown(content) : ''}
`; + div.innerHTML = `
${role === 'user' ? 'YOU' : 'AI'}
${role}
${content ? renderMarkdown(content) : ''}
${role === 'assistant' ? '
' : ''}
`; container.appendChild(div); - if (content && role === 'assistant') addCopyButtons(div); + if (content && role === 'assistant') { addCopyButtons(div); addMessageToolbar(div); } scrollToBottom(); return div; } @@ -1435,6 +1445,68 @@ function addCopyButtons(msgDiv) { }); } +function addMessageToolbar(msgDiv) { + const toolbar = msgDiv.querySelector('.msg-toolbar'); + if (!toolbar || toolbar.hasChildNodes()) return; + const textDiv = msgDiv.querySelector('.text'); + const getText = () => textDiv ? textDiv.textContent || textDiv.innerText : ''; + + const copyBtn = document.createElement('button'); + copyBtn.innerHTML = '📋'; + copyBtn.title = 'Copy response'; + copyBtn.onclick = () => { + const t = getText(); + if (navigator.clipboard) { + navigator.clipboard.writeText(t).then(() => { copyBtn.textContent = '✓'; setTimeout(() => copyBtn.innerHTML = '📋', 1500); }); + } + }; + + const printBtn = document.createElement('button'); + printBtn.innerHTML = '🖨️'; + printBtn.title = 'Print response'; + printBtn.onclick = () => { + const w = window.open('', '_blank', 'width=800,height=600'); + w.document.write(`Print${textDiv ? textDiv.innerHTML : ''}`); + w.document.close(); + setTimeout(() => { w.focus(); w.print(); }, 500); + }; + + const saveBtn = document.createElement('button'); + saveBtn.innerHTML = '💾'; + saveBtn.title = 'Save as .md'; + saveBtn.onclick = () => { + const t = getText(); + const blob = new Blob([t], { type: 'text/markdown' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `jarvischat-response-${Date.now()}.md`; + a.click(); + URL.revokeObjectURL(a.href); + }; + + const rating = { value: 0 }; + const upBtn = document.createElement('button'); + upBtn.innerHTML = '👍'; + upBtn.title = 'Rate up'; + upBtn.onclick = () => { + rating.value = rating.value === 1 ? 0 : 1; + upBtn.classList.toggle('active', rating.value === 1); + dnBtn.classList.toggle('active', false); + }; + + const dnBtn = document.createElement('button'); + dnBtn.innerHTML = '👎'; + dnBtn.title = 'Rate down'; + dnBtn.onclick = () => { + rating.value = rating.value === -1 ? 0 : -1; + dnBtn.classList.toggle('active', rating.value === -1); + upBtn.classList.toggle('active', false); + }; + + toolbar.append(copyBtn, printBtn, saveBtn, sep(), upBtn, dnBtn); + function sep() { const s = document.createElement('span'); s.className = 'sep'; return s; } +} + function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; } function scrollToBottom() { const c = document.getElementById('chatContainer'); c.scrollTop = c.scrollHeight; } function fallbackCopy(text, btn) {