mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-17 11:07:11 +08:00
feat: stream Bash progress and guard message actions during replies
This commit is contained in:
@@ -605,6 +605,18 @@
|
||||
color: inherit;
|
||||
}
|
||||
.tool-error-text { color: #f87171; }
|
||||
.tool-live-output:empty { display: none; }
|
||||
.tool-streaming .tool-live-output:not(:empty)::after {
|
||||
content: ' ';
|
||||
display: inline-block;
|
||||
width: 0.45em;
|
||||
height: 1em;
|
||||
margin-left: 0.15em;
|
||||
vertical-align: -0.15em;
|
||||
background: currentColor;
|
||||
animation: tool-cursor-blink 1s steps(1) infinite;
|
||||
}
|
||||
@keyframes tool-cursor-blink { 50% { opacity: 0; } }
|
||||
|
||||
/* Log level highlighting */
|
||||
.log-line { display: block; }
|
||||
@@ -1571,3 +1583,9 @@
|
||||
.delete-msg-btn:hover {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
|
||||
.edit-msg-btn:disabled,
|
||||
.delete-msg-btn:disabled {
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.35 !important;
|
||||
}
|
||||
|
||||
@@ -889,6 +889,26 @@ let pollGeneration = 0; // incremented on each restart to cancel stale poll lo
|
||||
let loadingContainers = {};
|
||||
let activeStreams = {}; // request_id -> EventSource
|
||||
let sessionActiveRequest = {}; // session_id -> request_id (in-flight stream per session)
|
||||
|
||||
function isCurrentSessionConversationActive() {
|
||||
return !!sessionActiveRequest[sessionId];
|
||||
}
|
||||
|
||||
function updateEditButtonsState() {
|
||||
const active = isCurrentSessionConversationActive();
|
||||
document.querySelectorAll('.edit-msg-btn, .delete-msg-btn').forEach(btn => {
|
||||
btn.disabled = active;
|
||||
if (btn.classList.contains('edit-msg-btn')) {
|
||||
btn.title = active
|
||||
? 'Reply is being generated; editing is temporarily unavailable.'
|
||||
: (currentLang === 'zh' ? '编辑消息' : 'Edit Message');
|
||||
} else {
|
||||
btn.title = active
|
||||
? 'Reply is being generated; deletion is temporarily unavailable.'
|
||||
: t('delete_message_title');
|
||||
}
|
||||
});
|
||||
}
|
||||
let streamBuffers = {}; // request_id -> { items: [event...], timestamp } for re-attach replay
|
||||
let isComposing = false;
|
||||
let appConfig = { use_agent: false, title: 'CowAgent', subtitle: '', providers: {}, api_bases: {} };
|
||||
@@ -1177,6 +1197,7 @@ messagesDiv.addEventListener('click', (e) => {
|
||||
const editBtn = e.target.closest('.edit-msg-btn');
|
||||
if (editBtn) {
|
||||
e.preventDefault();
|
||||
if (isCurrentSessionConversationActive()) return;
|
||||
const msgRoot = editBtn.closest('.user-message-group');
|
||||
if (msgRoot) editUserMessage(msgRoot);
|
||||
return;
|
||||
@@ -1197,6 +1218,7 @@ messagesDiv.addEventListener('click', (e) => {
|
||||
const deleteBtn = e.target.closest('.delete-msg-btn');
|
||||
if (deleteBtn) {
|
||||
e.preventDefault();
|
||||
if (isCurrentSessionConversationActive()) return;
|
||||
const userMsgEl = deleteBtn.closest('.user-message-group');
|
||||
if (!userMsgEl) return;
|
||||
|
||||
@@ -1993,6 +2015,7 @@ function copyToClipboard(text) {
|
||||
|
||||
// Edit user message: extract content, remove this and subsequent messages, fill input
|
||||
async function editUserMessage(msgEl) {
|
||||
if (isCurrentSessionConversationActive()) return;
|
||||
const rawContent = msgEl.dataset.rawContent;
|
||||
if (!rawContent) return;
|
||||
|
||||
@@ -2043,9 +2066,9 @@ async function editUserMessage(msgEl) {
|
||||
|
||||
// Fill input with the original content
|
||||
chatInput.value = rawContent;
|
||||
chatInput.style.height = 'auto';
|
||||
chatInput.style.height = chatInput.scrollHeight + 'px';
|
||||
chatInput.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
chatInput.focus();
|
||||
chatInput.selectionStart = chatInput.selectionEnd = chatInput.value.length;
|
||||
scrollChatToBottom();
|
||||
}
|
||||
|
||||
@@ -2250,7 +2273,7 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo, replayItems) {
|
||||
let contentEl = null; // .answer-content (final streaming answer)
|
||||
let mediaEl = null; // .media-content (images & file attachments)
|
||||
let accumulatedText = '';
|
||||
let currentToolEl = null;
|
||||
const toolElements = new Map();
|
||||
let currentReasoningEl = null; // live reasoning bubble
|
||||
let reasoningText = '';
|
||||
let reasoningStartTime = 0;
|
||||
@@ -2265,12 +2288,14 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo, replayItems) {
|
||||
const ownerSession = sessionId;
|
||||
const isActive = () => ownerSession === sessionId;
|
||||
sessionActiveRequest[ownerSession] = requestId;
|
||||
updateEditButtonsState();
|
||||
// Per-request event buffer used to rebuild the bubble on re-attach.
|
||||
const buffer = streamBuffers[requestId] || { items: [], timestamp };
|
||||
streamBuffers[requestId] = buffer;
|
||||
const clearOwnerRequest = () => {
|
||||
if (sessionActiveRequest[ownerSession] === requestId) {
|
||||
delete sessionActiveRequest[ownerSession];
|
||||
updateEditButtonsState();
|
||||
}
|
||||
delete streamBuffers[requestId];
|
||||
};
|
||||
@@ -2419,10 +2444,10 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo, replayItems) {
|
||||
contentEl.innerHTML = '';
|
||||
|
||||
// Add tool execution indicator (collapsible)
|
||||
currentToolEl = document.createElement('div');
|
||||
currentToolEl.className = 'agent-step agent-tool-step';
|
||||
const toolEl = document.createElement('div');
|
||||
toolEl.className = 'agent-step agent-tool-step tool-streaming';
|
||||
const argsStr = formatToolArgs(item.arguments || {});
|
||||
currentToolEl.innerHTML = `
|
||||
toolEl.innerHTML = `
|
||||
<div class="tool-header" onclick="this.parentElement.classList.toggle('expanded')">
|
||||
<i class="fas fa-cog fa-spin text-primary-400 flex-shrink-0 tool-icon"></i>
|
||||
<span class="tool-name">${item.tool}</span>
|
||||
@@ -2433,36 +2458,51 @@ function startSSE(requestId, loadingEl, timestamp, titleInfo, replayItems) {
|
||||
<div class="tool-detail-label">Input</div>
|
||||
<pre class="tool-detail-content">${argsStr}</pre>
|
||||
</div>
|
||||
<div class="tool-detail-section tool-output-section"></div>
|
||||
<div class="tool-detail-section tool-output-section">
|
||||
<div class="tool-detail-label tool-output-label">Output</div>
|
||||
<pre class="tool-detail-content tool-live-output"></pre>
|
||||
</div>
|
||||
</div>`;
|
||||
stepsEl.appendChild(currentToolEl);
|
||||
stepsEl.appendChild(toolEl);
|
||||
toolElements.set(item.tool_call_id, toolEl);
|
||||
|
||||
scrollChatToBottom();
|
||||
|
||||
} else if (item.type === 'tool_progress') {
|
||||
const toolEl = toolElements.get(item.tool_call_id);
|
||||
if (toolEl) {
|
||||
toolEl.classList.add('expanded');
|
||||
toolEl.querySelector('.tool-live-output').textContent = String(item.content || '');
|
||||
scrollChatToBottom();
|
||||
}
|
||||
|
||||
} else if (item.type === 'tool_end') {
|
||||
if (currentToolEl) {
|
||||
const toolEl = toolElements.get(item.tool_call_id);
|
||||
if (toolEl) {
|
||||
const isError = item.status !== 'success';
|
||||
const icon = currentToolEl.querySelector('.tool-icon');
|
||||
const icon = toolEl.querySelector('.tool-icon');
|
||||
icon.className = isError
|
||||
? 'fas fa-times text-red-400 flex-shrink-0 tool-icon'
|
||||
: 'fas fa-check text-primary-400 flex-shrink-0 tool-icon';
|
||||
|
||||
// Show execution time
|
||||
const nameEl = currentToolEl.querySelector('.tool-name');
|
||||
const nameEl = toolEl.querySelector('.tool-name');
|
||||
if (item.execution_time !== undefined) {
|
||||
nameEl.innerHTML += ` <span class="tool-time">${item.execution_time}s</span>`;
|
||||
}
|
||||
|
||||
// Fill output section
|
||||
const outputSection = currentToolEl.querySelector('.tool-output-section');
|
||||
if (outputSection && item.result) {
|
||||
outputSection.innerHTML = `
|
||||
<div class="tool-detail-label">${isError ? 'Error' : 'Output'}</div>
|
||||
<pre class="tool-detail-content ${isError ? 'tool-error-text' : ''}">${escapeHtml(String(item.result))}</pre>`;
|
||||
const outputLabel = toolEl.querySelector('.tool-output-label');
|
||||
const outputEl = toolEl.querySelector('.tool-live-output');
|
||||
if (outputLabel) outputLabel.textContent = isError ? 'Error' : 'Output';
|
||||
if (outputEl) {
|
||||
outputEl.textContent = item.result ? String(item.result) : '';
|
||||
outputEl.classList.toggle('tool-error-text', isError);
|
||||
}
|
||||
|
||||
if (isError) currentToolEl.classList.add('tool-failed');
|
||||
currentToolEl = null;
|
||||
toolEl.classList.remove('tool-streaming');
|
||||
if (isError) toolEl.classList.add('tool-failed');
|
||||
toolElements.delete(item.tool_call_id);
|
||||
}
|
||||
|
||||
} else if (item.type === 'image') {
|
||||
@@ -3256,6 +3296,7 @@ function loadHistory(page) {
|
||||
const sentinel = document.getElementById('history-load-more');
|
||||
const insertBefore = sentinel ? sentinel.nextSibling : messagesDiv.firstChild;
|
||||
messagesDiv.insertBefore(fragment, insertBefore);
|
||||
updateEditButtonsState();
|
||||
|
||||
// Manage the "load more" sentinel at the very top
|
||||
if (data.has_more) {
|
||||
@@ -3711,6 +3752,7 @@ function switchSession(newSessionId) {
|
||||
// Switching back re-attaches and resumes live streaming.
|
||||
|
||||
sessionId = newSessionId;
|
||||
updateEditButtonsState();
|
||||
localStorage.setItem(SESSION_ID_KEY, sessionId);
|
||||
|
||||
historyPage = 0;
|
||||
|
||||
@@ -432,7 +432,15 @@ class WebChannel(ChatChannel):
|
||||
elif event_type == "tool_execution_start":
|
||||
tool_name = data.get("tool_name", "tool")
|
||||
arguments = data.get("arguments", {})
|
||||
q.put({"type": "tool_start", "tool": tool_name, "arguments": arguments})
|
||||
q.put({"type": "tool_start", "tool_call_id": data.get("tool_call_id"), "tool": tool_name, "arguments": arguments})
|
||||
|
||||
elif event_type == "tool_execution_progress":
|
||||
q.put({
|
||||
"type": "tool_progress",
|
||||
"tool_call_id": data.get("tool_call_id"),
|
||||
"tool": data.get("tool_name", "tool"),
|
||||
"content": str(data.get("message", ""))[-32 * 1024:],
|
||||
})
|
||||
|
||||
elif event_type == "tool_execution_end":
|
||||
tool_name = data.get("tool_name", "tool")
|
||||
@@ -445,6 +453,7 @@ class WebChannel(ChatChannel):
|
||||
result_str = result_str[:2000] + "…"
|
||||
q.put({
|
||||
"type": "tool_end",
|
||||
"tool_call_id": data.get("tool_call_id"),
|
||||
"tool": tool_name,
|
||||
"status": status,
|
||||
"result": result_str,
|
||||
|
||||
Reference in New Issue
Block a user