Add files via upload

This commit is contained in:
公明
2026-04-03 22:57:38 +08:00
committed by GitHub
parent 4fd083ff37
commit f1a31a459c
5 changed files with 175 additions and 0 deletions
+61
View File
@@ -1573,6 +1573,67 @@ header {
letter-spacing: 0.01em;
}
/* 时间戳 + 删除本轮(与气泡分离,和「展开详情」同一视觉层级) */
.message-meta-footer {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
margin-top: 6px;
flex-wrap: wrap;
width: 100%;
min-height: 22px;
}
.message.user .message-meta-footer {
justify-content: flex-end;
}
.message.assistant .message-meta-footer {
justify-content: flex-start;
}
.message-delete-turn-btn {
position: static;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
color: var(--text-secondary, #888);
cursor: pointer;
opacity: 0.5;
flex-shrink: 0;
transition: opacity 0.2s ease, color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
}
.message:hover .message-meta-footer .message-delete-turn-btn {
opacity: 0.85;
}
.message-delete-turn-btn:hover {
color: #c62828;
background: rgba(198, 40, 40, 0.07);
border-color: rgba(198, 40, 40, 0.15);
opacity: 1;
}
.message-delete-turn-btn:focus-visible {
opacity: 1;
outline: 2px solid var(--accent-color, #0066ff);
outline-offset: 1px;
}
@media (hover: none) {
.message-delete-turn-btn {
opacity: 0.65;
}
}
/* 用户消息中的表格样式 */
.message.user .message-bubble .table-wrapper {
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
+3
View File
@@ -138,6 +138,9 @@
"expandDetail": "Expand details",
"noProcessDetail": "No process details (execution may be too fast or no detailed events)",
"copyMessageTitle": "Copy message",
"deleteTurnTitle": "Delete this turn",
"deleteTurnConfirm": "Delete this entire turn (user message and assistant reply)? This cannot be undone. The next reply will use only the remaining messages; saved context snapshots will be cleared.",
"deleteTurnFailed": "Failed to delete turn",
"emptyGroupConversations": "This group has no conversations yet.",
"noMatchingConversationsInGroup": "No matching conversations found.",
"noHistoryConversations": "No conversation history yet",
+3
View File
@@ -138,6 +138,9 @@
"expandDetail": "展开详情",
"noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)",
"copyMessageTitle": "复制消息内容",
"deleteTurnTitle": "删除本轮对话",
"deleteTurnConfirm": "确定删除本轮对话?将同时删除该轮用户消息与助手回复,且无法恢复;下次模型回复将仅基于剩余消息(已保存的上下文快照会清空并按剩余内容重建)。",
"deleteTurnFailed": "删除本轮失败",
"emptyGroupConversations": "该分组暂无对话",
"noMatchingConversationsInGroup": "未找到匹配的对话",
"noHistoryConversations": "暂无历史对话",
+62
View File
@@ -2452,6 +2452,7 @@ async function loadConversation(conversationId) {
const messageEl = document.getElementById(messageId);
if (messageEl && msg && msg.id) {
messageEl.dataset.backendMessageId = String(msg.id);
attachDeleteTurnButton(messageEl);
}
// 对于助手消息,总是渲染过程详情(即使没有processDetails也要显示展开详情按钮)
if (msg.role === 'assistant') {
@@ -2491,6 +2492,67 @@ async function loadConversation(conversationId) {
}
}
/** 「删除本轮」:与时间戳同一行(message-meta-footer),风格与复制按钮区区分 */
function attachDeleteTurnButton(messageEl) {
if (!messageEl || !messageEl.dataset.backendMessageId) return;
if (messageEl.querySelector('.message-delete-turn-btn')) return;
const content = messageEl.querySelector('.message-content');
if (!content) return;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'message-delete-turn-btn';
const title = typeof window.t === 'function' ? window.t('chat.deleteTurnTitle') : '删除本轮对话';
btn.title = title;
btn.setAttribute('aria-label', title);
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14zM10 11v6M14 11v6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>';
btn.onclick = function (e) {
e.stopPropagation();
e.preventDefault();
deleteConversationTurnFromUI(messageEl.dataset.backendMessageId);
};
const timeDiv = content.querySelector('.message-time');
let footer = content.querySelector('.message-meta-footer');
if (!footer && timeDiv && timeDiv.parentNode === content) {
footer = document.createElement('div');
footer.className = 'message-meta-footer';
timeDiv.parentNode.insertBefore(footer, timeDiv);
footer.appendChild(timeDiv);
}
if (footer) {
footer.appendChild(btn);
} else {
content.appendChild(btn);
}
}
/** 删除锚点所在整轮(后端:该轮 user 至下一轮 user 之前),并清空 ReAct 快照 */
async function deleteConversationTurnFromUI(anchorBackendMessageId) {
if (!currentConversationId || !anchorBackendMessageId) return;
const confirmMsg = typeof window.t === 'function' ? window.t('chat.deleteTurnConfirm') : '确定删除本轮对话?';
if (!confirm(confirmMsg)) return;
try {
const response = await apiFetch(`/api/conversations/${currentConversationId}/delete-turn`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messageId: anchorBackendMessageId })
});
let data = {};
try {
data = await response.json();
} catch (e) { /* ignore */ }
if (!response.ok) {
throw new Error(data.error || data.message || 'delete failed');
}
await loadConversation(currentConversationId);
if (typeof loadConversations === 'function') loadConversations();
if (typeof loadConversationsWithGroups === 'function') loadConversationsWithGroups();
} catch (error) {
console.error('delete turn failed:', error);
const failed = typeof window.t === 'function' ? window.t('chat.deleteTurnFailed') : '删除本轮失败';
alert(failed + ': ' + (error && error.message ? error.message : error));
}
}
// 删除对话
async function deleteConversation(conversationId, skipConfirm = false) {
// 确认删除(如果调用者没有跳过确认)
+46
View File
@@ -700,10 +700,45 @@ function convertProgressToDetails(progressId, assistantMessageId) {
scrollChatMessagesToBottomIfPinned(insertWasPinned);
}
/** 将后端消息 UUID 绑定到助手气泡,供删除本轮 / 过程详情懒加载(domId 为前端 msg-* */
function applyBackendMessageIdToAssistantDom(domAssistantId, backendMessageId) {
if (!domAssistantId || !backendMessageId) return;
const el = document.getElementById(domAssistantId);
if (!el) return;
el.dataset.backendMessageId = String(backendMessageId);
if (typeof attachDeleteTurnButton === 'function') {
attachDeleteTurnButton(el);
}
}
/** 将后端用户消息 ID 绑定到最后一条尚未绑定 backendMessageId 的用户气泡 */
function applyBackendMessageIdToLastUser(backendMessageId) {
if (!backendMessageId) return;
const users = document.querySelectorAll('#chat-messages .message.user');
if (!users.length) return;
const lastUser = users[users.length - 1];
if (lastUser.dataset.backendMessageId) return;
lastUser.dataset.backendMessageId = String(backendMessageId);
if (typeof attachDeleteTurnButton === 'function') {
attachDeleteTurnButton(lastUser);
}
}
// 处理流式事件
function handleStreamEvent(event, progressElement, progressId,
getAssistantId, setAssistantId, getMcpIds, setMcpIds) {
const streamScrollWasPinned = isChatMessagesPinnedToBottom();
// 不依赖进度时间线;在首条 SSE 即可绑定用户消息 ID
if (event.type === 'message_saved') {
const d = event.data || {};
if (d.userMessageId) {
applyBackendMessageIdToLastUser(d.userMessageId);
}
scrollChatMessagesToBottomIfPinned(streamScrollWasPinned);
return;
}
const timeline = document.getElementById(progressId + '-timeline');
if (!timeline) return;
@@ -1173,6 +1208,9 @@ function handleStreamEvent(event, progressElement, progressId,
{
const preferredMessageId = event.data && event.data.messageId ? event.data.messageId : null;
const { assistantId, assistantElement } = upsertTerminalAssistantMessage(event.message, preferredMessageId);
if (assistantId && preferredMessageId) {
applyBackendMessageIdToAssistantDom(assistantId, preferredMessageId);
}
if (assistantElement) {
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {
@@ -1306,6 +1344,11 @@ function handleStreamEvent(event, progressElement, progressId,
integrateProgressToMCPSection(progressId, assistantIdFinal, mcpIds);
responseStreamStateByProgressId.delete(progressId);
const respMid = responseData.messageId;
if (respMid) {
applyBackendMessageIdToAssistantDom(assistantIdFinal, respMid);
}
setTimeout(() => {
collapseAllProgressDetails(assistantIdFinal, progressId);
}, 3000);
@@ -1344,6 +1387,9 @@ function handleStreamEvent(event, progressElement, progressId,
{
const preferredMessageId = event.data && event.data.messageId ? event.data.messageId : null;
const { assistantId, assistantElement } = upsertTerminalAssistantMessage(event.message, preferredMessageId);
if (assistantId && preferredMessageId) {
applyBackendMessageIdToAssistantDom(assistantId, preferredMessageId);
}
if (assistantElement) {
const detailsId = 'process-details-' + assistantId;
if (!document.getElementById(detailsId)) {