mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
- Add edit/delete/regenerate for user and bot messages
- Add language labels and copy buttons to code blocks - Enhance drag-and-drop to full chat view - Fix data consistency bugs in message operations - Use RLock to prevent deadlock in conversation store"
This commit is contained in:
@@ -184,6 +184,8 @@ const I18N = {
|
||||
today: '今天', yesterday: '昨天', earlier: '更早',
|
||||
delete_session_confirm: '确认删除该会话?所有消息将被清除。',
|
||||
delete_session_title: '删除会话',
|
||||
delete_message_confirm: '确认删除这条消息?',
|
||||
delete_message_title: '删除消息',
|
||||
untitled_session: '新对话',
|
||||
context_cleared: '— 以上内容已从上下文中移除 —',
|
||||
tip_new_chat: '新建对话',
|
||||
@@ -206,6 +208,10 @@ const I18N = {
|
||||
confirm_cancel: '取消',
|
||||
error_send: '发送失败,请稍后再试。', error_timeout: '请求超时,请再试一次。',
|
||||
thinking_in_progress: '思考中...', thinking_done: '已深度思考', thinking_duration: '耗时',
|
||||
edit_message: '编辑消息',
|
||||
regenerate_response: '重新生成',
|
||||
edit_save: '保存并发送',
|
||||
edit_cancel: '取消',
|
||||
},
|
||||
en: {
|
||||
console: 'Console',
|
||||
@@ -380,6 +386,8 @@ const I18N = {
|
||||
today: 'Today', yesterday: 'Yesterday', earlier: 'Earlier',
|
||||
delete_session_confirm: 'Delete this session? All messages will be removed.',
|
||||
delete_session_title: 'Delete Session',
|
||||
delete_message_confirm: 'Delete this message?',
|
||||
delete_message_title: 'Delete Message',
|
||||
untitled_session: 'New Chat',
|
||||
context_cleared: '— Context above has been cleared —',
|
||||
tip_new_chat: 'New Chat',
|
||||
@@ -402,6 +410,10 @@ const I18N = {
|
||||
confirm_cancel: 'Cancel',
|
||||
error_send: 'Failed to send. Please try again.', error_timeout: 'Request timeout. Please try again.',
|
||||
thinking_in_progress: 'Thinking...', thinking_done: 'Thought', thinking_duration: 'Duration',
|
||||
edit_message: 'Edit message',
|
||||
regenerate_response: 'Regenerate',
|
||||
edit_save: 'Save and send',
|
||||
edit_cancel: 'Cancel',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -821,11 +833,45 @@ function renderMarkdown(text) {
|
||||
let html = md.render(text);
|
||||
html = _rewriteLocalImgSrc(html);
|
||||
// Order matters: video first (more specific), then image.
|
||||
return injectImagePreviews(injectVideoPlayers(html));
|
||||
html = injectImagePreviews(injectVideoPlayers(html));
|
||||
// Note: Code block headers are added via DOM manipulation after insertion
|
||||
// See addCodeBlockHeadersToElement()
|
||||
return html;
|
||||
}
|
||||
catch (e) { return text.replace(/\n/g, '<br>'); }
|
||||
}
|
||||
|
||||
function _addCodeBlockHeaders(container) {
|
||||
// Add header with language label and copy button to each <pre> block using DOM manipulation
|
||||
const preBlocks = container.querySelectorAll('pre');
|
||||
preBlocks.forEach(pre => {
|
||||
if (pre.parentElement && pre.parentElement.classList.contains('code-block-wrapper')) return;
|
||||
|
||||
const codeEl = pre.querySelector('code');
|
||||
if (!codeEl) return;
|
||||
|
||||
const langClass = Array.from(codeEl.classList).find(c => c.startsWith('language-'));
|
||||
const language = langClass ? langClass.replace('language-', '') : 'code';
|
||||
const langLabel = language.charAt(0).toUpperCase() + language.slice(1);
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'code-block-wrapper';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'code-block-header';
|
||||
header.innerHTML = `
|
||||
<span class="code-block-lang">${langLabel}</span>
|
||||
<button class="code-copy-btn" title="Copy code">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
`;
|
||||
|
||||
pre.parentNode.insertBefore(wrapper, pre);
|
||||
wrapper.appendChild(header);
|
||||
wrapper.appendChild(pre);
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Chat Module
|
||||
// =====================================================================
|
||||
@@ -1085,6 +1131,22 @@ messagesDiv.addEventListener('scroll', () => {
|
||||
|
||||
// Intercept internal navigation links in chat messages
|
||||
messagesDiv.addEventListener('click', (e) => {
|
||||
// Code block copy button
|
||||
const codeCopyBtn = e.target.closest('.code-copy-btn');
|
||||
if (codeCopyBtn) {
|
||||
e.preventDefault();
|
||||
const wrapper = codeCopyBtn.closest('.code-block-wrapper');
|
||||
const codeEl = wrapper && wrapper.querySelector('pre code');
|
||||
if (codeEl) {
|
||||
const codeText = codeEl.textContent;
|
||||
copyToClipboard(codeText).then(() => {
|
||||
const icon = codeCopyBtn.querySelector('i');
|
||||
if (icon) { icon.className = 'fas fa-check'; setTimeout(() => { icon.className = 'fas fa-copy'; }, 1500); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const copyBtn = e.target.closest('.copy-msg-btn');
|
||||
if (copyBtn) {
|
||||
e.preventDefault();
|
||||
@@ -1092,13 +1154,97 @@ messagesDiv.addEventListener('click', (e) => {
|
||||
const answerEl = msgRoot && msgRoot.querySelector('.answer-content');
|
||||
const rawMd = answerEl && answerEl.dataset.rawMd;
|
||||
if (rawMd) {
|
||||
navigator.clipboard.writeText(rawMd).then(() => {
|
||||
copyToClipboard(rawMd).then(() => {
|
||||
const icon = copyBtn.querySelector('i');
|
||||
if (icon) { icon.className = 'fas fa-check'; setTimeout(() => { icon.className = 'fas fa-copy'; }, 1500); }
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Edit user message
|
||||
const editBtn = e.target.closest('.edit-msg-btn');
|
||||
if (editBtn) {
|
||||
e.preventDefault();
|
||||
const msgRoot = editBtn.closest('.user-message-group');
|
||||
if (msgRoot) editUserMessage(msgRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regenerate bot response
|
||||
const regenerateBtn = e.target.closest('.regenerate-msg-btn');
|
||||
if (regenerateBtn) {
|
||||
e.preventDefault();
|
||||
const botMsgRoot = regenerateBtn.closest('.flex.gap-3');
|
||||
if (botMsgRoot) regenerateResponse(botMsgRoot);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete message
|
||||
const deleteBtn = e.target.closest('.delete-msg-btn');
|
||||
if (deleteBtn) {
|
||||
e.preventDefault();
|
||||
const userMsgEl = deleteBtn.closest('.user-message-group');
|
||||
const botMsgEl = deleteBtn.closest('.flex.gap-3:not(.user-message-group)');
|
||||
|
||||
if (userMsgEl) {
|
||||
showConfirmModal(t('delete_message_title'), t('delete_message_confirm'), () => {
|
||||
let nextSibling = userMsgEl.nextElementSibling;
|
||||
let botReplyEl = null;
|
||||
while (nextSibling) {
|
||||
if (nextSibling.classList && nextSibling.classList.contains('flex') && nextSibling.classList.contains('gap-3') && !nextSibling.classList.contains('user-message-group')) {
|
||||
botReplyEl = nextSibling;
|
||||
break;
|
||||
}
|
||||
nextSibling = nextSibling.nextElementSibling;
|
||||
}
|
||||
userMsgEl.remove();
|
||||
if (botReplyEl) botReplyEl.remove();
|
||||
|
||||
const userSeq = userMsgEl.dataset.seq;
|
||||
if (userSeq) {
|
||||
fetch('/api/messages/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_id: sessionId, user_seq: parseInt(userSeq) })
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') console.log(`Deleted ${data.deleted} messages`);
|
||||
}).catch(err => console.error('Failed to delete:', err));
|
||||
}
|
||||
});
|
||||
} else if (botMsgEl) {
|
||||
showConfirmModal(t('delete_message_title'), t('delete_message_confirm'), () => {
|
||||
// Find the preceding user message to get its seq
|
||||
let prevUserEl = botMsgEl.previousElementSibling;
|
||||
while (prevUserEl && !prevUserEl.classList.contains('user-message-group')) {
|
||||
prevUserEl = prevUserEl.previousElementSibling;
|
||||
}
|
||||
|
||||
// Delete from database (keep user message, only delete bot reply)
|
||||
if (prevUserEl) {
|
||||
const userSeq = prevUserEl.dataset.seq;
|
||||
if (userSeq) {
|
||||
fetch('/api/messages/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
user_seq: parseInt(userSeq),
|
||||
delete_user: false
|
||||
})
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.status === 'success') console.log(`Deleted ${data.deleted} bot reply messages`);
|
||||
}).catch(err => console.error('Failed to delete bot reply:', err));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from DOM
|
||||
botMsgEl.remove();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const a = e.target.closest('a');
|
||||
if (!a) return;
|
||||
const href = a.getAttribute('href') || '';
|
||||
@@ -1376,14 +1522,83 @@ document.addEventListener('click', (e) => {
|
||||
hideAttachMenu();
|
||||
});
|
||||
|
||||
// Drag-and-drop support on chat input area
|
||||
// Drag-and-drop support on entire chat view
|
||||
const chatView = document.getElementById('view-chat');
|
||||
const chatInputArea = chatInput.closest('.flex-shrink-0');
|
||||
chatInputArea.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); chatInputArea.classList.add('drag-over'); });
|
||||
chatInputArea.addEventListener('dragleave', (e) => { e.preventDefault(); e.stopPropagation(); chatInputArea.classList.remove('drag-over'); });
|
||||
chatInputArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); e.stopPropagation();
|
||||
|
||||
// Create drag overlay for visual feedback
|
||||
let dragOverlay = document.getElementById('drag-overlay');
|
||||
if (!dragOverlay) {
|
||||
dragOverlay = document.createElement('div');
|
||||
dragOverlay.id = 'drag-overlay';
|
||||
dragOverlay.className = 'drag-overlay hidden';
|
||||
dragOverlay.innerHTML = `
|
||||
<div class="drag-overlay-content">
|
||||
<i class="fas fa-cloud-arrow-up"></i>
|
||||
<p>Drop files here to upload</p>
|
||||
</div>
|
||||
`;
|
||||
chatView.appendChild(dragOverlay);
|
||||
}
|
||||
|
||||
let dragCounter = 0;
|
||||
|
||||
function showDragOverlay() {
|
||||
dragOverlay.classList.remove('hidden');
|
||||
dragOverlay.classList.add('active');
|
||||
}
|
||||
|
||||
function hideDragOverlay() {
|
||||
dragOverlay.classList.remove('active');
|
||||
dragOverlay.classList.add('hidden');
|
||||
}
|
||||
|
||||
chatView.addEventListener('dragenter', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter++;
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
showDragOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
chatView.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
chatInputArea.classList.add('drag-over');
|
||||
});
|
||||
|
||||
chatView.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter--;
|
||||
if (dragCounter === 0) {
|
||||
hideDragOverlay();
|
||||
chatInputArea.classList.remove('drag-over');
|
||||
}
|
||||
});
|
||||
|
||||
chatView.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter = 0;
|
||||
hideDragOverlay();
|
||||
chatInputArea.classList.remove('drag-over');
|
||||
if (e.dataTransfer.files.length) handleFileSelect(e.dataTransfer.files);
|
||||
if (e.dataTransfer.files.length) {
|
||||
handleFileSelect(e.dataTransfer.files);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('dragover', (e) => {
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('drop', (e) => {
|
||||
if (e.dataTransfer.types.includes('Files')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Paste image support
|
||||
@@ -1761,6 +1976,183 @@ function addUserVoiceMessage(audioUrl, caption, timestamp) {
|
||||
scrollChatToBottom(true);
|
||||
}
|
||||
|
||||
// Clipboard helper with fallback for non-HTTPS environments
|
||||
function copyToClipboard(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
return navigator.clipboard.writeText(text);
|
||||
}
|
||||
// Fallback for HTTP environments
|
||||
return new Promise((resolve, reject) => {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = text;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-999999px';
|
||||
textArea.style.top = '-999999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.focus();
|
||||
textArea.select();
|
||||
try {
|
||||
document.execCommand('copy') ? resolve() : reject(new Error('Copy failed'));
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
} finally {
|
||||
textArea.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Edit user message: extract content, remove this and subsequent messages, fill input
|
||||
async function editUserMessage(msgEl) {
|
||||
const rawContent = msgEl.dataset.rawContent;
|
||||
if (!rawContent) return;
|
||||
|
||||
// Delete this message and ALL subsequent messages from database (cascade)
|
||||
// Must await to ensure delete completes before user sends a new message
|
||||
const userSeq = msgEl.dataset.seq;
|
||||
if (userSeq) {
|
||||
try {
|
||||
const resp = await fetch('/api/messages/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
user_seq: parseInt(userSeq),
|
||||
delete_user: true,
|
||||
cascade: true
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.status === 'success') console.log(`Deleted ${data.deleted} old messages`);
|
||||
} catch (err) {
|
||||
console.error('Failed to delete old messages:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Find all subsequent messages (this message and everything after it)
|
||||
const messagesToRemove = [];
|
||||
let current = msgEl;
|
||||
while (current) {
|
||||
if (current.classList && (current.classList.contains('user-message-group') || current.classList.contains('flex'))) {
|
||||
messagesToRemove.push(current);
|
||||
}
|
||||
current = current.nextElementSibling;
|
||||
}
|
||||
|
||||
// Remove all messages from this one onwards
|
||||
messagesToRemove.forEach(el => {
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
|
||||
// Fill input with the original content
|
||||
chatInput.value = rawContent;
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = chatInput.scrollHeight + 'px';
|
||||
chatInput.focus();
|
||||
scrollChatToBottom();
|
||||
}
|
||||
|
||||
// Regenerate bot response: find the preceding user message and resend it
|
||||
async function regenerateResponse(botMsgEl) {
|
||||
let prevEl = botMsgEl.previousElementSibling;
|
||||
while (prevEl && !prevEl.classList.contains('user-message-group')) {
|
||||
prevEl = prevEl.previousElementSibling;
|
||||
}
|
||||
|
||||
if (!prevEl) {
|
||||
console.warn('No preceding user message found');
|
||||
return;
|
||||
}
|
||||
|
||||
const userContent = prevEl.dataset.rawContent;
|
||||
if (!userContent) {
|
||||
console.warn('No content in preceding user message');
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete both the old user message AND bot reply from database
|
||||
// (because /message will create a fresh user message + new bot reply)
|
||||
// Must await to ensure delete completes before /message is sent
|
||||
const userSeq = prevEl.dataset.seq;
|
||||
if (userSeq) {
|
||||
try {
|
||||
const resp = await fetch('/api/messages/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: sessionId,
|
||||
user_seq: parseInt(userSeq),
|
||||
delete_user: true
|
||||
})
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.status === 'success') console.log(`Deleted ${data.deleted} old messages`);
|
||||
} catch (err) {
|
||||
console.error('Failed to delete old messages:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove both the old user message and bot message from DOM
|
||||
if (prevEl.parentNode) prevEl.parentNode.removeChild(prevEl);
|
||||
if (botMsgEl.parentNode) botMsgEl.parentNode.removeChild(botMsgEl);
|
||||
|
||||
// Re-add the user message to DOM (so it appears before the loading indicator)
|
||||
addUserMessage(userContent, new Date());
|
||||
|
||||
// Show loading indicator
|
||||
const loadingEl = addLoadingIndicator();
|
||||
|
||||
// Resend the message
|
||||
const timestamp = new Date();
|
||||
const body = { session_id: sessionId, message: userContent, stream: true, timestamp: timestamp.toISOString(), lang: currentLang };
|
||||
|
||||
const MAX_RETRIES = 2;
|
||||
const RETRY_DELAY_MS = 1000;
|
||||
|
||||
function postWithRetry(attempt) {
|
||||
fetch('/message', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
if (data.inline_reply) {
|
||||
loadingEl.remove();
|
||||
addBotMessage(data.inline_reply, new Date());
|
||||
} else if (data.stream) {
|
||||
setSendBtnCancelMode(data.request_id);
|
||||
startSSE(data.request_id, loadingEl, timestamp, null);
|
||||
} else {
|
||||
loadingContainers[data.request_id] = loadingEl;
|
||||
}
|
||||
} else {
|
||||
loadingEl.remove();
|
||||
addBotMessage(t('error_send'), new Date());
|
||||
resetSendBtnSendMode();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if (err.name === 'AbortError') {
|
||||
loadingEl.remove();
|
||||
addBotMessage(t('error_timeout'), new Date());
|
||||
resetSendBtnSendMode();
|
||||
return;
|
||||
}
|
||||
if (attempt < MAX_RETRIES) {
|
||||
console.warn(`[regenerateResponse] attempt ${attempt + 1} failed, retrying...`, err);
|
||||
setTimeout(() => postWithRetry(attempt + 1), RETRY_DELAY_MS * (attempt + 1));
|
||||
return;
|
||||
}
|
||||
loadingEl.remove();
|
||||
addBotMessage(t('error_send'), new Date());
|
||||
resetSendBtnSendMode();
|
||||
});
|
||||
}
|
||||
|
||||
postWithRetry(0);
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
// Do NOT branch on sendBtnMode here: Enter should always send (so
|
||||
// typing "/cancel" submits normally). Cancel is wired only to the
|
||||
@@ -2252,7 +2644,7 @@ function startPolling() {
|
||||
|
||||
function createUserMessageEl(content, timestamp, attachments) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'flex justify-end px-4 sm:px-6 py-3';
|
||||
el.className = 'flex justify-end px-4 sm:px-6 py-3 user-message-group';
|
||||
|
||||
let attachHtml = '';
|
||||
if (attachments && attachments.length > 0) {
|
||||
@@ -2277,9 +2669,19 @@ function createUserMessageEl(content, timestamp, attachments) {
|
||||
<div class="bg-primary-400 text-white rounded-2xl px-4 py-2.5 text-sm leading-relaxed msg-content user-bubble">
|
||||
${attachHtml}${textHtml}
|
||||
</div>
|
||||
<div class="text-xs text-slate-400 dark:text-slate-500 mt-1.5 text-right">${formatTime(timestamp)}</div>
|
||||
<div class="flex items-center justify-end gap-2 mt-1.5">
|
||||
<button class="edit-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${t('edit_message')}">
|
||||
<i class="fas fa-pen-to-square"></i>
|
||||
</button>
|
||||
<button class="delete-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors cursor-pointer" title="${t('delete_message_title')}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<span class="text-xs text-slate-400 dark:text-slate-500">${formatTime(timestamp)}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
// Store raw content for editing
|
||||
el.dataset.rawContent = content || '';
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -2487,6 +2889,12 @@ function createBotMessageEl(content, timestamp, requestId, msg) {
|
||||
<button class="copy-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${currentLang === 'zh' ? '复制' : 'Copy'}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
<button class="regenerate-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-primary-400 dark:hover:text-primary-400 transition-colors cursor-pointer" title="${t('regenerate_response')}">
|
||||
<i class="fas fa-rotate-right"></i>
|
||||
</button>
|
||||
<button class="delete-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-red-500 dark:hover:text-red-400 transition-colors cursor-pointer" title="${t('delete_message_title')}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<button class="speak-msg-btn text-xs text-slate-300 dark:text-slate-600 hover:text-slate-500 dark:hover:text-slate-400 transition-colors cursor-pointer" title="${t('speak_msg')}" style="display:none;">
|
||||
<i class="fas fa-volume-up"></i>
|
||||
</button>
|
||||
@@ -2709,6 +3117,10 @@ function loadHistory(page) {
|
||||
const el = msg.role === 'user'
|
||||
? createUserMessageEl(msg.content, ts)
|
||||
: createBotMessageEl(msg.content || '', ts, null, msg);
|
||||
// Store seq for delete functionality
|
||||
if (msg._seq !== undefined) {
|
||||
el.dataset.seq = msg._seq;
|
||||
}
|
||||
fragment.appendChild(el);
|
||||
});
|
||||
|
||||
@@ -3285,6 +3697,8 @@ function applyHighlighting(container) {
|
||||
hljsLib.highlightElement(block);
|
||||
}
|
||||
});
|
||||
// Add language labels and copy buttons to code blocks
|
||||
_addCodeBlockHeaders(root);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user