|
38 | 38 | <!-- 聊天视图 --> |
39 | 39 | <div class="chat-container" id="chatContainer"> |
40 | 40 | <div class="messages" id="messages"></div> |
41 | | - <div class="loading" id="loading"> |
42 | | - <div class="spinner"></div> |
43 | | - <span>Thinking...</span> |
44 | | - </div> |
45 | 41 | <div class="composer"> |
46 | 42 | <!-- Skills Popup --> |
47 | 43 | <div class="skills-popup" id="skillsPopup"> |
|
97 | 93 | const promptMinRows = Number(promptInput?.getAttribute("rows")) || 3; |
98 | 94 | const promptMaxRows = 10; |
99 | 95 | const sendButton = document.getElementById("send"); |
100 | | - const loading = document.getElementById("loading"); |
101 | 96 | const sessionSelector = document.getElementById("sessionSelector"); |
102 | 97 | const sessionSelectorTitle = document.getElementById("sessionSelectorTitle"); |
103 | 98 | const sessionSelectorTitleText = document.getElementById("sessionSelectorTitleText"); |
|
1269 | 1264 | return `${seconds}s`; |
1270 | 1265 | } |
1271 | 1266 |
|
1272 | | - function updateLoadingText() { |
1273 | | - const loadingSpan = loading.querySelector('span'); |
1274 | | - if (!loadingSpan) { |
1275 | | - return; |
1276 | | - } |
| 1267 | + let currentThinkingBubbleElement = null; // 当前 thinking 气泡 DOM 元素 |
| 1268 | + |
| 1269 | + function getThinkingContentText() { |
1277 | 1270 | if (currentRunningProcesses && Object.keys(currentRunningProcesses).length > 0) { |
1278 | | - // 显示第一个运行中的进程(通常只有一个进程在运行) |
1279 | 1271 | const firstPid = Object.keys(currentRunningProcesses)[0]; |
1280 | 1272 | const process = currentRunningProcesses[firstPid]; |
1281 | 1273 | const elapsed = formatElapsedTime(process.startTime); |
1282 | | - loadingSpan.textContent = `(${elapsed}) ${process.command}`; |
1283 | | - return; |
| 1274 | + return `(${elapsed}) ${process.command}`; |
1284 | 1275 | } |
1285 | 1276 |
|
1286 | 1277 | if (currentLlmStreamProgress && currentLlmStreamProgress.startedAt) { |
1287 | 1278 | const startedAt = new Date(currentLlmStreamProgress.startedAt).getTime(); |
1288 | 1279 | const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1000)); |
| 1280 | + const formattedTokens = currentLlmStreamProgress.formattedTokens || "0"; |
1289 | 1281 | if (elapsedSeconds >= 3) { |
1290 | | - const formattedTokens = currentLlmStreamProgress.formattedTokens || "0"; |
1291 | | - loadingSpan.textContent = `Thinking... (${elapsedSeconds}s) · ↓ ${formattedTokens} tokens`; |
1292 | | - return; |
| 1282 | + return `(${elapsedSeconds}s) · ↓ ${formattedTokens} tokens`; |
1293 | 1283 | } |
1294 | 1284 | } |
1295 | 1285 |
|
1296 | | - loadingSpan.textContent = 'Thinking...'; |
| 1286 | + return "Processing..."; |
| 1287 | + } |
| 1288 | + |
| 1289 | + function updateThinkingBubbleContent() { |
| 1290 | + if (!currentThinkingBubbleElement) return; |
| 1291 | + const statusEl = currentThinkingBubbleElement.querySelector(".thinking-status"); |
| 1292 | + if (statusEl) { |
| 1293 | + statusEl.textContent = getThinkingContentText(); |
| 1294 | + } |
| 1295 | + } |
| 1296 | + |
| 1297 | + function removeThinkingBubble() { |
| 1298 | + if (currentThinkingBubbleElement) { |
| 1299 | + currentThinkingBubbleElement.remove(); |
| 1300 | + currentThinkingBubbleElement = null; |
| 1301 | + } |
| 1302 | + } |
| 1303 | + |
| 1304 | + function createThinkingBubble(shouldConnectToPrev) { |
| 1305 | + removeThinkingBubble(); |
| 1306 | + |
| 1307 | + const bubble = document.createElement("div"); |
| 1308 | + bubble.className = "bubble assistant"; |
| 1309 | + bubble.dataset.thinkingLive = "true"; |
| 1310 | + |
| 1311 | + const header = document.createElement("div"); |
| 1312 | + header.className = "bubble-collapsible-header"; |
| 1313 | + |
| 1314 | + const dotClass = shouldConnectToPrev ? "bubble-dot spinner-dot connect-to-prev" : "bubble-dot spinner-dot"; |
| 1315 | + const statusText = getThinkingContentText(); |
| 1316 | + header.innerHTML = ` |
| 1317 | + <span class="${dotClass}"></span> |
| 1318 | + <span class="bubble-title"> |
| 1319 | + <span class="bubble-title-text"><b>Thinking</b> <span class="thinking-status">${escapeHtml(statusText)}</span></span> |
| 1320 | + </span> |
| 1321 | + `; |
| 1322 | + |
| 1323 | + bubble.appendChild(header); |
| 1324 | + messages.appendChild(bubble); |
| 1325 | + messages.scrollTop = messages.scrollHeight; |
| 1326 | + |
| 1327 | + currentThinkingBubbleElement = bubble; |
| 1328 | + |
| 1329 | + // 连接线 |
| 1330 | + if (shouldConnectToPrev) { |
| 1331 | + requestAnimationFrame(() => { |
| 1332 | + updateConnectionLine(bubble); |
| 1333 | + }); |
| 1334 | + } |
| 1335 | + requestAnimationFrame(() => { |
| 1336 | + updateAllConnectionLines(); |
| 1337 | + }); |
1297 | 1338 | } |
1298 | 1339 |
|
1299 | 1340 | function setLoading(isLoading, status) { |
1300 | | - loading.classList.toggle("active", isLoading); |
1301 | 1341 | if (status !== undefined) { |
1302 | 1342 | currentSessionStatus = status; |
1303 | 1343 | } |
|
1312 | 1352 | } |
1313 | 1353 |
|
1314 | 1354 | if (isLoading) { |
1315 | | - loadingUpdateTimer = setInterval(updateLoadingText, 1000); |
| 1355 | + // 创建 thinking bubble,连到上一条非 user 消息 |
| 1356 | + const shouldConnect = lastMessageRole !== null && lastMessageRole !== "user"; |
| 1357 | + createThinkingBubble(shouldConnect); |
| 1358 | + loadingUpdateTimer = setInterval(updateThinkingBubbleContent, 1000); |
| 1359 | + } else { |
| 1360 | + removeThinkingBubble(); |
1316 | 1361 | } |
1317 | 1362 |
|
1318 | | - // 更新初始文本 |
1319 | | - updateLoadingText(); |
1320 | | - |
1321 | 1363 | // 切换图标 |
1322 | 1364 | if (isLoading) { |
1323 | 1365 | sendIcon.style.display = "none"; |
|
1892 | 1934 | } else { |
1893 | 1935 | currentLlmStreamProgress = progress; |
1894 | 1936 | } |
1895 | | - updateLoadingText(); |
| 1937 | + updateThinkingBubbleContent(); |
1896 | 1938 | } else if (message.type === "userMessage") { |
1897 | 1939 | // 显示用户消息(纯文本) |
1898 | 1940 | addBubble(message.content || "", "user", message.meta, false); |
|
0 commit comments