Add full OpenWebUI-like features: retry, thumbs up/down, copy, edit, continue, stop generation
This commit is contained in:
parent
4cce5d0995
commit
347f14067a
@ -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); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<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">
|
||||
<!-- Chat header -->
|
||||
<header class="h-14 border-b border-balloon-purple/20 flex items-center justify-between px-6 bg-surface/30">
|
||||
<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">
|
||||
<span class="w-2 h-2 bg-balloon-yellow rounded-full animate-pulse"></span>
|
||||
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"
|
||||
></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 -->
|
||||
<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">
|
||||
@ -207,6 +285,9 @@
|
||||
let currentConversationId = null;
|
||||
let isGenerating = false;
|
||||
let attachments = [];
|
||||
let messageHistory = []; // Store messages for retry/edit
|
||||
let currentReader = null; // For stop functionality
|
||||
let lastUserMessage = ''; // For retry
|
||||
|
||||
// DOM elements
|
||||
const messagesList = document.getElementById('messagesList');
|
||||
@ -215,10 +296,12 @@
|
||||
const chatForm = document.getElementById('chatForm');
|
||||
const messageInput = document.getElementById('messageInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const stopBtn = document.getElementById('stopBtn');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const attachmentsPreview = document.getElementById('attachmentsPreview');
|
||||
const conversationsList = document.getElementById('conversationsList');
|
||||
const chatTitle = document.getElementById('chatTitle');
|
||||
const editTitleBtn = document.getElementById('editTitleBtn');
|
||||
const newChatBtn = document.getElementById('newChatBtn');
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
@ -252,6 +335,12 @@
|
||||
// Logout
|
||||
logoutBtn.addEventListener('click', handleLogout);
|
||||
|
||||
// Stop generation
|
||||
stopBtn.addEventListener('click', stopGeneration);
|
||||
|
||||
// Edit title
|
||||
editTitleBtn.addEventListener('click', editChatTitle);
|
||||
|
||||
// Sidebar toggle (mobile)
|
||||
sidebarToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('-translate-x-full');
|
||||
@ -318,6 +407,7 @@
|
||||
|
||||
async function loadConversation(convId) {
|
||||
currentConversationId = convId;
|
||||
messageHistory = [];
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/conversations/${convId}/messages`);
|
||||
@ -327,7 +417,8 @@
|
||||
welcomeMessage.classList.add('hidden');
|
||||
|
||||
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
|
||||
@ -351,8 +442,13 @@
|
||||
// Hide welcome message
|
||||
welcomeMessage.classList.add('hidden');
|
||||
|
||||
// Store for retry
|
||||
lastUserMessage = 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
|
||||
messageInput.value = '';
|
||||
@ -361,9 +457,14 @@
|
||||
attachmentsPreview.classList.add('hidden');
|
||||
attachmentsPreview.innerHTML = '';
|
||||
|
||||
// Start generation
|
||||
// Generate response
|
||||
await generateResponse();
|
||||
}
|
||||
|
||||
async function generateResponse(retry = false) {
|
||||
isGenerating = true;
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.classList.add('hidden');
|
||||
stopBtn.classList.remove('hidden');
|
||||
updateStatus('Thinking...', true);
|
||||
|
||||
try {
|
||||
@ -371,9 +472,9 @@
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
message: retry ? lastUserMessage : messageHistory[messageHistory.length - 1]?.content || '',
|
||||
conversation_id: currentConversationId,
|
||||
attachments: attachments
|
||||
attachments: []
|
||||
})
|
||||
});
|
||||
|
||||
@ -381,15 +482,17 @@
|
||||
currentConversationId = response.headers.get('X-Conversation-Id');
|
||||
|
||||
// Create assistant message container
|
||||
const assistantMsg = appendMessage('assistant', '', null, true);
|
||||
const assistantMsgId = 'msg-' + Date.now();
|
||||
const assistantMsg = appendMessage('assistant', '', null, assistantMsgId, true);
|
||||
|
||||
// Read stream
|
||||
const reader = response.body.getReader();
|
||||
// Store reader for stop functionality
|
||||
currentReader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let fullContent = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
const { done, value } = await currentReader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
@ -406,6 +509,7 @@
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.content) {
|
||||
fullContent += parsed.content;
|
||||
appendToMessage(assistantMsg, parsed.content);
|
||||
}
|
||||
} catch (e) {
|
||||
@ -415,42 +519,105 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Store in history
|
||||
messageHistory.push({ role: 'assistant', content: fullContent });
|
||||
|
||||
// Refresh conversations list
|
||||
loadConversations();
|
||||
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
console.error('Chat error:', error);
|
||||
appendMessage('assistant', 'An error occurred. Please try again.', null);
|
||||
appendMessage('assistant', 'An error occurred. Please try again.', null, 'msg-error');
|
||||
}
|
||||
} finally {
|
||||
isGenerating = false;
|
||||
sendBtn.disabled = false;
|
||||
currentReader = null;
|
||||
sendBtn.classList.remove('hidden');
|
||||
stopBtn.classList.add('hidden');
|
||||
updateStatus('Ready', false);
|
||||
}
|
||||
}
|
||||
|
||||
function appendMessage(role, content, attachments, streaming = false) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = `message-fade-in ${role === 'user' ? 'flex justify-end' : ''}`;
|
||||
function stopGeneration() {
|
||||
if (currentReader) {
|
||||
currentReader.cancel();
|
||||
currentReader = null;
|
||||
}
|
||||
isGenerating = false;
|
||||
sendBtn.classList.remove('hidden');
|
||||
stopBtn.classList.add('hidden');
|
||||
updateStatus('Stopped', false);
|
||||
}
|
||||
|
||||
const isThinking = content.includes('[Thinking...]') || content.includes('[Searching');
|
||||
function appendMessage(role, content, attachments, msgId, streaming = false) {
|
||||
const wrapper = document.createElement('div');
|
||||
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') || content.includes('[Creating');
|
||||
|
||||
let contentHtml = '';
|
||||
|
||||
if (role === 'user') {
|
||||
contentHtml = `
|
||||
<div class="max-w-[80%] bg-balloon-purple/30 rounded-2xl rounded-tr-sm px-4 py-3">
|
||||
<p class="whitespace-pre-wrap">${escapeHtml(content)}</p>
|
||||
<div class="max-w-[80%]">
|
||||
<div class="bg-balloon-purple/30 rounded-2xl rounded-tr-sm px-4 py-3">
|
||||
<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>
|
||||
`;
|
||||
} else {
|
||||
contentHtml = `
|
||||
<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">
|
||||
<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)}
|
||||
</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>
|
||||
`;
|
||||
@ -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, '<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
|
||||
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;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user