diff --git a/moxie/web/templates/chat.html b/moxie/web/templates/chat.html index f19d4a7..b06e80f 100644 --- a/moxie/web/templates/chat.html +++ b/moxie/web/templates/chat.html @@ -61,6 +61,69 @@ border-radius: 12px; box-shadow: 0 4px 20px rgba(106, 68, 119, 0.3); } + + /* Message actions visibility */ + .message-wrapper:hover .message-actions { + opacity: 1; + } + .message-actions { + opacity: 0; + transition: opacity 0.2s ease; + } + + /* Action button styles */ + .action-btn { + padding: 6px; + border-radius: 6px; + transition: all 0.2s ease; + } + .action-btn:hover { + background: rgba(79, 154, 195, 0.2); + } + .action-btn.active { + color: #ECDC67; + } + .action-btn.negative { + color: #EA744C; + } + .action-btn.positive { + color: #4F9AC3; + } + + /* Edit textarea */ + .edit-textarea { + background: rgba(11, 12, 28, 0.8); + border: 1px solid #6A4477; + border-radius: 8px; + padding: 12px; + width: 100%; + min-height: 80px; + color: #F0F0F0; + resize: vertical; + } + .edit-textarea:focus { + outline: none; + border-color: #4F9AC3; + } + + /* Copy notification */ + .copy-toast { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: #6A4477; + color: #F0F0F0; + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + animation: fadeUp 0.3s ease; + } + @keyframes fadeUp { + from { opacity: 0; transform: translate(-50%, 10px); } + to { opacity: 1; transform: translate(-50%, 0); } + } @@ -122,7 +185,14 @@
-
New Chat
+
+
New Chat
+ +
Ready @@ -186,6 +256,14 @@ class="flex-1 bg-transparent resize-none outline-none text-text-main placeholder-text-main/30 max-h-32 py-2" > + + + + +
`; } else { @@ -448,9 +587,37 @@
MOXIE
-
+
${renderMarkdown(content)}
+ +
+ + + + + +
`; @@ -471,18 +638,14 @@ function appendToMessage(element, content) { const contentDiv = element.querySelector('.prose'); if (contentDiv) { - // Check if it's a thinking phase message const currentText = contentDiv.textContent; - // Handle thinking phase specially - if (content.includes('[Thinking') || content.includes('[Searching') || content.includes('[Generating')) { + if (content.includes('[Thinking') || content.includes('[Searching') || content.includes('[Generating') || content.includes('[Creating')) { contentDiv.classList.add('thinking-content', 'rounded-lg', 'px-4', 'py-2'); } else if (!content.includes('[') && contentDiv.classList.contains('thinking-content')) { - // Remove thinking style for regular content contentDiv.classList.remove('thinking-content'); } - // Append content const fullContent = currentText + content; contentDiv.innerHTML = renderMarkdown(fullContent); @@ -491,11 +654,10 @@ } function renderMarkdown(text) { - // Simple markdown rendering let html = escapeHtml(text); // Code blocks - html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
'); + html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '
$2
'); // Inline code html = html.replace(/`([^`]+)`/g, '$1'); @@ -512,6 +674,145 @@ return html; } + // Message Actions + function copyMessage(msgId) { + const wrapper = document.querySelector(`[data-msg-id="${msgId}"]`); + const contentEl = wrapper?.querySelector('.user-content, .assistant-content'); + if (contentEl) { + navigator.clipboard.writeText(contentEl.textContent).then(() => { + showToast('Copied to clipboard'); + }); + } + } + + function editMessage(msgId) { + const wrapper = document.querySelector(`[data-msg-id="${msgId}"]`); + const contentEl = wrapper?.querySelector('.user-content'); + if (!contentEl) return; + + const originalContent = contentEl.textContent; + + // Create edit form + const editForm = document.createElement('div'); + editForm.className = 'mt-2'; + editForm.innerHTML = ` + +
+ + +
+ `; + + contentEl.style.display = 'none'; + wrapper.querySelector('.bg-balloon-purple\\/30').appendChild(editForm); + + // Focus textarea + const textarea = editForm.querySelector('textarea'); + textarea.focus(); + textarea.selectionStart = textarea.value.length; + + // Save handler + editForm.querySelector('.save-edit').addEventListener('click', () => { + const newContent = textarea.value.trim(); + if (newContent && newContent !== originalContent) { + contentEl.textContent = newContent; + lastUserMessage = newContent; + // Update history + const idx = messageHistory.findIndex(m => m.role === 'user' && m.content === originalContent); + if (idx !== -1) messageHistory[idx].content = newContent; + + // Remove all messages after this one + let found = false; + const toRemove = []; + messagesList.querySelectorAll('.message-wrapper').forEach(el => { + if (found) toRemove.push(el); + if (el.dataset.msgId === msgId) found = true; + }); + toRemove.forEach(el => el.remove()); + + // Regenerate response + generateResponse(true); + } + contentEl.style.display = ''; + editForm.remove(); + }); + + // Cancel handler + editForm.querySelector('.cancel-edit').addEventListener('click', () => { + contentEl.style.display = ''; + editForm.remove(); + }); + } + + function regenerateResponse() { + if (isGenerating) return; + + // Remove last assistant message + const lastAssistant = messageHistory.findLast(m => m.role === 'assistant'); + if (lastAssistant) { + const wrappers = messagesList.querySelectorAll('.message-wrapper'); + for (let i = wrappers.length - 1; i >= 0; i--) { + if (wrappers[i].dataset.role === 'assistant') { + wrappers[i].remove(); + break; + } + } + messageHistory = messageHistory.filter(m => m !== lastAssistant); + } + + generateResponse(true); + } + + function continueGeneration() { + if (isGenerating) return; + + // Add a continuation prompt + messageHistory.push({ role: 'user', content: 'Continue' }); + lastUserMessage = 'Continue'; + generateResponse(); + } + + function rateMessage(msgId, rating) { + const wrapper = document.querySelector(`[data-msg-id="${msgId}"]`); + const positiveBtn = wrapper?.querySelector('.positive'); + const negativeBtn = wrapper?.querySelector('.negative'); + + if (rating === 'positive') { + positiveBtn?.classList.toggle('active'); + negativeBtn?.classList.remove('active'); + showToast('Thanks for the feedback!'); + } else { + negativeBtn?.classList.toggle('active'); + positiveBtn?.classList.remove('active'); + showToast('Thanks for the feedback - we\'ll work on improving'); + } + + // TODO: Send rating to backend for analytics + } + + function editChatTitle() { + const currentTitle = chatTitle.textContent; + const newTitle = prompt('Enter new chat title:', currentTitle); + if (newTitle && newTitle !== currentTitle && currentConversationId) { + fetch(`/api/conversations/${currentConversationId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: newTitle }) + }).then(() => { + chatTitle.textContent = newTitle; + loadConversations(); + }); + } + } + + function showToast(message) { + const toast = document.createElement('div'); + toast.className = 'copy-toast'; + toast.textContent = message; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 2000); + } + function scrollToBottom() { messagesContainer.scrollTop = messagesContainer.scrollHeight; } @@ -540,7 +841,6 @@ const data = await response.json(); attachments.push(data.id); - // Show preview const preview = document.createElement('div'); preview.className = 'relative group'; @@ -557,7 +857,6 @@ `; } - // Remove button const removeBtn = document.createElement('button'); removeBtn.className = 'absolute -top-2 -right-2 w-5 h-5 bg-balloon-red rounded-full text-xs opacity-0 group-hover:opacity-100 transition-opacity'; removeBtn.innerHTML = '×'; @@ -581,6 +880,7 @@ async function startNewChat() { currentConversationId = null; + messageHistory = []; messagesList.innerHTML = ''; welcomeMessage.classList.remove('hidden'); chatTitle.textContent = 'New Chat';