Add full OpenWebUI-like features: retry, thumbs up/down, copy, edit, continue, stop generation

This commit is contained in:
Z User 2026-03-24 06:36:20 +00:00
parent 4cce5d0995
commit 347f14067a

View File

@ -61,6 +61,69 @@
border-radius: 12px; border-radius: 12px;
box-shadow: 0 4px 20px rgba(106, 68, 119, 0.3); 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); }
}
</style> </style>
</head> </head>
<body class="bg-background text-text-main h-screen flex overflow-hidden"> <body class="bg-background text-text-main h-screen flex overflow-hidden">
@ -122,7 +185,14 @@
<main class="flex-1 flex flex-col min-w-0"> <main class="flex-1 flex flex-col min-w-0">
<!-- Chat header --> <!-- Chat header -->
<header class="h-14 border-b border-balloon-purple/20 flex items-center justify-between px-6 bg-surface/30"> <header class="h-14 border-b border-balloon-purple/20 flex items-center justify-between px-6 bg-surface/30">
<div id="chatTitle" class="font-medium">New Chat</div> <div class="flex items-center gap-3">
<div id="chatTitle" class="font-medium">New Chat</div>
<button id="editTitleBtn" class="p-1 hover:bg-balloon-purple/20 rounded transition-colors opacity-50 hover:opacity-100" title="Rename chat">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
</svg>
</button>
</div>
<div id="statusIndicator" class="flex items-center gap-2 text-sm text-text-main/50"> <div id="statusIndicator" class="flex items-center gap-2 text-sm text-text-main/50">
<span class="w-2 h-2 bg-balloon-yellow rounded-full animate-pulse"></span> <span class="w-2 h-2 bg-balloon-yellow rounded-full animate-pulse"></span>
Ready 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" class="flex-1 bg-transparent resize-none outline-none text-text-main placeholder-text-main/30 max-h-32 py-2"
></textarea> ></textarea>
<!-- Stop/Regenerate button (hidden by default) -->
<button type="button" id="stopBtn" class="hidden p-2 bg-balloon-red hover:bg-balloon-red/80 rounded-lg transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"></path>
</svg>
</button>
<!-- Send button --> <!-- Send button -->
<button type="submit" id="sendBtn" class="p-2 bg-balloon-blue hover:bg-balloon-blue/80 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"> <button type="submit" id="sendBtn" class="p-2 bg-balloon-blue hover:bg-balloon-blue/80 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@ -207,6 +285,9 @@
let currentConversationId = null; let currentConversationId = null;
let isGenerating = false; let isGenerating = false;
let attachments = []; let attachments = [];
let messageHistory = []; // Store messages for retry/edit
let currentReader = null; // For stop functionality
let lastUserMessage = ''; // For retry
// DOM elements // DOM elements
const messagesList = document.getElementById('messagesList'); const messagesList = document.getElementById('messagesList');
@ -215,10 +296,12 @@
const chatForm = document.getElementById('chatForm'); const chatForm = document.getElementById('chatForm');
const messageInput = document.getElementById('messageInput'); const messageInput = document.getElementById('messageInput');
const sendBtn = document.getElementById('sendBtn'); const sendBtn = document.getElementById('sendBtn');
const stopBtn = document.getElementById('stopBtn');
const fileInput = document.getElementById('fileInput'); const fileInput = document.getElementById('fileInput');
const attachmentsPreview = document.getElementById('attachmentsPreview'); const attachmentsPreview = document.getElementById('attachmentsPreview');
const conversationsList = document.getElementById('conversationsList'); const conversationsList = document.getElementById('conversationsList');
const chatTitle = document.getElementById('chatTitle'); const chatTitle = document.getElementById('chatTitle');
const editTitleBtn = document.getElementById('editTitleBtn');
const newChatBtn = document.getElementById('newChatBtn'); const newChatBtn = document.getElementById('newChatBtn');
const logoutBtn = document.getElementById('logoutBtn'); const logoutBtn = document.getElementById('logoutBtn');
const sidebar = document.getElementById('sidebar'); const sidebar = document.getElementById('sidebar');
@ -252,6 +335,12 @@
// Logout // Logout
logoutBtn.addEventListener('click', handleLogout); logoutBtn.addEventListener('click', handleLogout);
// Stop generation
stopBtn.addEventListener('click', stopGeneration);
// Edit title
editTitleBtn.addEventListener('click', editChatTitle);
// Sidebar toggle (mobile) // Sidebar toggle (mobile)
sidebarToggle.addEventListener('click', () => { sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('-translate-x-full'); sidebar.classList.toggle('-translate-x-full');
@ -318,6 +407,7 @@
async function loadConversation(convId) { async function loadConversation(convId) {
currentConversationId = convId; currentConversationId = convId;
messageHistory = [];
try { try {
const response = await fetch(`/api/conversations/${convId}/messages`); const response = await fetch(`/api/conversations/${convId}/messages`);
@ -327,7 +417,8 @@
welcomeMessage.classList.add('hidden'); welcomeMessage.classList.add('hidden');
messages.forEach(msg => { messages.forEach(msg => {
appendMessage(msg.role, msg.content, msg.attachments); messageHistory.push({ role: msg.role, content: msg.content });
appendMessage(msg.role, msg.content, msg.attachments, msg.id);
}); });
// Update sidebar selection // Update sidebar selection
@ -351,8 +442,13 @@
// Hide welcome message // Hide welcome message
welcomeMessage.classList.add('hidden'); welcomeMessage.classList.add('hidden');
// Store for retry
lastUserMessage = message;
// Add user message // Add user message
appendMessage('user', message, attachments); const userMsgId = 'msg-' + Date.now();
appendMessage('user', message, attachments, userMsgId);
messageHistory.push({ role: 'user', content: message });
// Clear input // Clear input
messageInput.value = ''; messageInput.value = '';
@ -361,9 +457,14 @@
attachmentsPreview.classList.add('hidden'); attachmentsPreview.classList.add('hidden');
attachmentsPreview.innerHTML = ''; attachmentsPreview.innerHTML = '';
// Start generation // Generate response
await generateResponse();
}
async function generateResponse(retry = false) {
isGenerating = true; isGenerating = true;
sendBtn.disabled = true; sendBtn.classList.add('hidden');
stopBtn.classList.remove('hidden');
updateStatus('Thinking...', true); updateStatus('Thinking...', true);
try { try {
@ -371,9 +472,9 @@
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
message: message, message: retry ? lastUserMessage : messageHistory[messageHistory.length - 1]?.content || '',
conversation_id: currentConversationId, conversation_id: currentConversationId,
attachments: attachments attachments: []
}) })
}); });
@ -381,15 +482,17 @@
currentConversationId = response.headers.get('X-Conversation-Id'); currentConversationId = response.headers.get('X-Conversation-Id');
// Create assistant message container // Create assistant message container
const assistantMsg = appendMessage('assistant', '', null, true); const assistantMsgId = 'msg-' + Date.now();
const assistantMsg = appendMessage('assistant', '', null, assistantMsgId, true);
// Read stream // Store reader for stop functionality
const reader = response.body.getReader(); currentReader = response.body.getReader();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
let buffer = ''; let buffer = '';
let fullContent = '';
while (true) { while (true) {
const { done, value } = await reader.read(); const { done, value } = await currentReader.read();
if (done) break; if (done) break;
buffer += decoder.decode(value, { stream: true }); buffer += decoder.decode(value, { stream: true });
@ -406,6 +509,7 @@
try { try {
const parsed = JSON.parse(data); const parsed = JSON.parse(data);
if (parsed.content) { if (parsed.content) {
fullContent += parsed.content;
appendToMessage(assistantMsg, parsed.content); appendToMessage(assistantMsg, parsed.content);
} }
} catch (e) { } catch (e) {
@ -415,32 +519,67 @@
} }
} }
// Store in history
messageHistory.push({ role: 'assistant', content: fullContent });
// Refresh conversations list // Refresh conversations list
loadConversations(); loadConversations();
} catch (error) { } catch (error) {
console.error('Chat error:', error); if (error.name !== 'AbortError') {
appendMessage('assistant', 'An error occurred. Please try again.', null); console.error('Chat error:', error);
appendMessage('assistant', 'An error occurred. Please try again.', null, 'msg-error');
}
} finally { } finally {
isGenerating = false; isGenerating = false;
sendBtn.disabled = false; currentReader = null;
sendBtn.classList.remove('hidden');
stopBtn.classList.add('hidden');
updateStatus('Ready', false); updateStatus('Ready', false);
} }
} }
function appendMessage(role, content, attachments, streaming = false) { function stopGeneration() {
if (currentReader) {
currentReader.cancel();
currentReader = null;
}
isGenerating = false;
sendBtn.classList.remove('hidden');
stopBtn.classList.add('hidden');
updateStatus('Stopped', false);
}
function appendMessage(role, content, attachments, msgId, streaming = false) {
const wrapper = document.createElement('div'); const wrapper = document.createElement('div');
wrapper.className = `message-fade-in ${role === 'user' ? 'flex justify-end' : ''}`; wrapper.className = `message-wrapper message-fade-in ${role === 'user' ? 'flex justify-end' : ''}`;
wrapper.dataset.msgId = msgId || '';
wrapper.dataset.role = role;
const isThinking = content.includes('[Thinking...]') || content.includes('[Searching'); const isThinking = content.includes('[Thinking...]') || content.includes('[Searching') || content.includes('[Creating');
let contentHtml = ''; let contentHtml = '';
if (role === 'user') { if (role === 'user') {
contentHtml = ` contentHtml = `
<div class="max-w-[80%] bg-balloon-purple/30 rounded-2xl rounded-tr-sm px-4 py-3"> <div class="max-w-[80%]">
<p class="whitespace-pre-wrap">${escapeHtml(content)}</p> <div class="bg-balloon-purple/30 rounded-2xl rounded-tr-sm px-4 py-3">
${attachments && attachments.length ? `<div class="mt-2 text-xs text-text-main/50">${attachments.length} attachment(s)</div>` : ''} <p class="whitespace-pre-wrap user-content">${escapeHtml(content)}</p>
${attachments && attachments.length ? `<div class="mt-2 text-xs text-text-main/50">${attachments.length} attachment(s)</div>` : ''}
</div>
<!-- User message actions -->
<div class="message-actions flex items-center gap-1 mt-1 justify-end">
<button class="action-btn edit-btn" title="Edit message" onclick="editMessage('${msgId}')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
</svg>
</button>
<button class="action-btn" title="Copy" onclick="copyMessage('${msgId}')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
</div>
</div> </div>
`; `;
} else { } else {
@ -448,9 +587,37 @@
<div class="flex gap-3 ${streaming ? 'typing-cursor' : ''}"> <div class="flex gap-3 ${streaming ? 'typing-cursor' : ''}">
<img src="/static/moxie_logo.jpg" alt="MOXIE" class="w-8 h-8 rounded-full flex-shrink-0 mt-1"> <img src="/static/moxie_logo.jpg" alt="MOXIE" class="w-8 h-8 rounded-full flex-shrink-0 mt-1">
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="prose prose-invert max-w-none ${isThinking ? 'thinking-content rounded-lg px-4 py-2' : ''}"> <div class="prose prose-invert max-w-none ${isThinking ? 'thinking-content rounded-lg px-4 py-2' : ''} assistant-content">
${renderMarkdown(content)} ${renderMarkdown(content)}
</div> </div>
<!-- Assistant message actions -->
<div class="message-actions flex items-center gap-1 mt-2">
<button class="action-btn copy-btn" title="Copy" onclick="copyMessage('${msgId}')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
<button class="action-btn" title="Regenerate" onclick="regenerateResponse()">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
</button>
<button class="action-btn positive" title="Good response" onclick="rateMessage('${msgId}', 'positive')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 10h4.764a2 2 0 011.789 2.894l-3.5 7A2 2 0 0115.263 21h-4.017c-.163 0-.326-.02-.485-.06L7 20m7-10V5a2 2 0 00-2-2h-.095c-.5 0-.905.405-.905.905 0 .714-.211 1.412-.608 2.006L7 11v9m7-10h-2M7 20H5a2 2 0 01-2-2v-6a2 2 0 012-2h2.5"></path>
</svg>
</button>
<button class="action-btn negative" title="Bad response" onclick="rateMessage('${msgId}', 'negative')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14H5.236a2 2 0 01-1.789-2.894l3.5-7A2 2 0 018.736 3h4.018a2 2 0 01.485.06l3.76.94m-7 10v5a2 2 0 002 2h.096c.5 0 .905-.405.905-.904 0-.715.211-1.413.608-2.008L17 13V4m-7 10h2m5-10h2a2 2 0 012 2v6a2 2 0 01-2 2h-2.5"></path>
</svg>
</button>
<button class="action-btn" title="Continue generation" onclick="continueGeneration()">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"></path>
</svg>
</button>
</div>
</div> </div>
</div> </div>
`; `;
@ -471,18 +638,14 @@
function appendToMessage(element, content) { function appendToMessage(element, content) {
const contentDiv = element.querySelector('.prose'); const contentDiv = element.querySelector('.prose');
if (contentDiv) { if (contentDiv) {
// Check if it's a thinking phase message
const currentText = contentDiv.textContent; const currentText = contentDiv.textContent;
// Handle thinking phase specially if (content.includes('[Thinking') || content.includes('[Searching') || content.includes('[Generating') || content.includes('[Creating')) {
if (content.includes('[Thinking') || content.includes('[Searching') || content.includes('[Generating')) {
contentDiv.classList.add('thinking-content', 'rounded-lg', 'px-4', 'py-2'); contentDiv.classList.add('thinking-content', 'rounded-lg', 'px-4', 'py-2');
} else if (!content.includes('[') && contentDiv.classList.contains('thinking-content')) { } else if (!content.includes('[') && contentDiv.classList.contains('thinking-content')) {
// Remove thinking style for regular content
contentDiv.classList.remove('thinking-content'); contentDiv.classList.remove('thinking-content');
} }
// Append content
const fullContent = currentText + content; const fullContent = currentText + content;
contentDiv.innerHTML = renderMarkdown(fullContent); contentDiv.innerHTML = renderMarkdown(fullContent);
@ -491,11 +654,10 @@
} }
function renderMarkdown(text) { function renderMarkdown(text) {
// Simple markdown rendering
let html = escapeHtml(text); let html = escapeHtml(text);
// Code blocks // Code blocks
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-background/50 rounded-lg p-3 overflow-x-auto"><code>$2</code></pre>'); html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="bg-background/50 rounded-lg p-3 overflow-x-auto my-2"><code>$2</code></pre>');
// Inline code // Inline code
html = html.replace(/`([^`]+)`/g, '<code class="bg-background/50 px-1.5 py-0.5 rounded text-balloon-yellow">$1</code>'); html = html.replace(/`([^`]+)`/g, '<code class="bg-background/50 px-1.5 py-0.5 rounded text-balloon-yellow">$1</code>');
@ -512,6 +674,145 @@
return html; 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 = `
<textarea class="edit-textarea">${escapeHtml(originalContent)}</textarea>
<div class="flex gap-2 mt-2">
<button class="px-3 py-1 bg-balloon-blue hover:bg-balloon-blue/80 rounded text-sm save-edit">Save</button>
<button class="px-3 py-1 bg-surface hover:bg-surface/80 rounded text-sm cancel-edit">Cancel</button>
</div>
`;
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() { function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight; messagesContainer.scrollTop = messagesContainer.scrollHeight;
} }
@ -540,7 +841,6 @@
const data = await response.json(); const data = await response.json();
attachments.push(data.id); attachments.push(data.id);
// Show preview
const preview = document.createElement('div'); const preview = document.createElement('div');
preview.className = 'relative group'; preview.className = 'relative group';
@ -557,7 +857,6 @@
`; `;
} }
// Remove button
const removeBtn = document.createElement('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.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 = '×'; removeBtn.innerHTML = '×';
@ -581,6 +880,7 @@
async function startNewChat() { async function startNewChat() {
currentConversationId = null; currentConversationId = null;
messageHistory = [];
messagesList.innerHTML = ''; messagesList.innerHTML = '';
welcomeMessage.classList.remove('hidden'); welcomeMessage.classList.remove('hidden');
chatTitle.textContent = 'New Chat'; chatTitle.textContent = 'New Chat';