mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-20 23:04:45 +02:00
Add files via upload
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -138,6 +138,9 @@
|
||||
"expandDetail": "展开详情",
|
||||
"noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)",
|
||||
"copyMessageTitle": "复制消息内容",
|
||||
"deleteTurnTitle": "删除本轮对话",
|
||||
"deleteTurnConfirm": "确定删除本轮对话?将同时删除该轮用户消息与助手回复,且无法恢复;下次模型回复将仅基于剩余消息(已保存的上下文快照会清空并按剩余内容重建)。",
|
||||
"deleteTurnFailed": "删除本轮失败",
|
||||
"emptyGroupConversations": "该分组暂无对话",
|
||||
"noMatchingConversationsInGroup": "未找到匹配的对话",
|
||||
"noHistoryConversations": "暂无历史对话",
|
||||
|
||||
@@ -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) {
|
||||
// 确认删除(如果调用者没有跳过确认)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user