From 87ceaad92b62efd7449516b29f71435301060bdb Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Sun, 30 Nov 2025 00:09:05 +0800 Subject: [PATCH] feat: improve ui feedback --- static/src/App.vue | 1 + static/src/app.ts | 150 ++++++++++++------ static/src/components/chat/ChatArea.vue | 30 ++++ static/src/components/panels/LeftPanel.vue | 11 +- static/src/composables/useLegacySocket.ts | 107 +++++++++---- static/src/stores/chat.ts | 41 ++++- static/src/stores/file.ts | 12 +- .../styles/components/chat/_chat-area.scss | 71 +++++++++ .../styles/components/panels/_left-panel.scss | 12 ++ 9 files changed, 346 insertions(+), 89 deletions(-) diff --git a/static/src/App.vue b/static/src/App.vue index 5a30ab6..c3e2901 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -69,6 +69,7 @@ @toggle-panel-menu="togglePanelMenu" @select-panel="selectPanelMode" @open-file-manager="openGuiFileManager" + @toggle-thinking-mode="handleQuickModeToggle" />
{ console.warn('CSRF token 初始化失败:', err); @@ -289,7 +297,7 @@ const appOptions = { currentConversationId: { immediate: false, handler(newValue, oldValue) { - console.log('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); + debugLog('currentConversationId 变化', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); this.logMessageState('watch:currentConversationId', { oldValue, newValue, skipConversationHistoryReload: this.skipConversationHistoryReload }); if (!newValue || typeof newValue !== 'string' || newValue.startsWith('temp_')) { return; @@ -593,7 +601,7 @@ const appOptions = { }, logMessageState(action, extra = {}) { const count = Array.isArray(this.messages) ? this.messages.length : 'N/A'; - console.log('[Messages]', { + debugLog('[Messages]', { action, count, conversationId: this.currentConversationId, @@ -913,9 +921,49 @@ const appOptions = { this.toolActionIndex.clear(); }, + hasPendingToolActions() { + const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0; + if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) { + return true; + } + if (!Array.isArray(this.messages)) { + return false; + } + return this.messages.some(msg => { + if (!msg || msg.role !== 'assistant' || !Array.isArray(msg.actions)) { + return false; + } + return msg.actions.some(action => { + if (!action || action.type !== 'tool' || !action.tool) { + return false; + } + if (action.tool.awaiting_content) { + return true; + } + const status = typeof action.tool.status === 'string' + ? action.tool.status.toLowerCase() + : ''; + return !status || ['preparing', 'running', 'pending', 'queued'].includes(status); + }); + }); + }, + + maybeResetStreamingState(reason = 'unspecified') { + if (!this.streamingMessage) { + return false; + } + if (this.hasPendingToolActions()) { + return false; + } + this.streamingMessage = false; + this.stopRequested = false; + debugLog('流式状态已结束', { reason }); + return true; + }, + // 完整重置所有状态 resetAllStates(reason = 'unspecified') { - console.log('重置所有前端状态', { reason, conversationId: this.currentConversationId }); + debugLog('重置所有前端状态', { reason, conversationId: this.currentConversationId }); this.logMessageState('resetAllStates:before-cleanup', { reason }); this.fileHideContextMenu(); @@ -960,7 +1008,7 @@ const appOptions = { this.toolSetSettingsLoading(false); this.toolSetSettings([]); - console.log('前端状态重置完成'); + debugLog('前端状态重置完成'); this._scrollListenerReady = false; this.$nextTick(() => { this.ensureScrollListener(); @@ -983,7 +1031,7 @@ const appOptions = { async loadInitialData() { try { - console.log('加载初始数据...'); + debugLog('加载初始数据...'); await this.fileFetchTree(); await this.focusFetchFiles(); @@ -1024,7 +1072,7 @@ const appOptions = { await this.loadToolSettings(true); - console.log('初始数据加载完成'); + debugLog('初始数据加载完成'); } catch (error) { console.error('加载初始数据失败:', error); } @@ -1110,7 +1158,7 @@ const appOptions = { this.promoteConversationToTop(this.currentConversationId); } this.hasMoreConversations = data.data.has_more; - console.log(`已加载 ${this.conversations.length} 个对话`); + debugLog(`已加载 ${this.conversations.length} 个对话`); if (this.conversationsOffset === 0 && !this.currentConversationId && this.conversations.length > 0) { const latestConversation = this.conversations[0]; @@ -1138,11 +1186,11 @@ const appOptions = { }, async loadConversation(conversationId) { - console.log('加载对话:', conversationId); + debugLog('加载对话:', conversationId); this.logMessageState('loadConversation:start', { conversationId }); if (conversationId === this.currentConversationId) { - console.log('已是当前对话,跳过加载'); + debugLog('已是当前对话,跳过加载'); return; } @@ -1154,7 +1202,7 @@ const appOptions = { const result = await response.json(); if (result.success) { - console.log('对话加载API成功:', result); + debugLog('对话加载API成功:', result); // 2. 更新当前对话信息 this.skipConversationHistoryReload = true; @@ -1210,16 +1258,16 @@ const appOptions = { // ========================================== async fetchAndDisplayHistory() { if (this.historyLoading) { - console.log('历史消息正在加载,跳过重复请求'); + debugLog('历史消息正在加载,跳过重复请求'); return; } this.historyLoading = true; try { - console.log('开始获取历史对话内容...'); + debugLog('开始获取历史对话内容...'); this.logMessageState('fetchAndDisplayHistory:start', { conversationId: this.currentConversationId }); if (!this.currentConversationId || this.currentConversationId.startsWith('temp_')) { - console.log('没有当前对话ID,跳过历史加载'); + debugLog('没有当前对话ID,跳过历史加载'); return; } @@ -1232,7 +1280,7 @@ const appOptions = { // 备用方案:通过状态API获取 const statusResponse = await fetch('/api/status'); const status = await statusResponse.json(); - console.log('系统状态:', status); + debugLog('系统状态:', status); this.applyStatusSnapshot(status); // 如果状态中有对话历史字段 @@ -1241,16 +1289,16 @@ const appOptions = { return; } - console.log('备用方案也无法获取历史消息'); + debugLog('备用方案也无法获取历史消息'); return; } const messagesData = await messagesResponse.json(); - console.log('获取到消息数据:', messagesData); + debugLog('获取到消息数据:', messagesData); if (messagesData.success && messagesData.data && messagesData.data.messages) { const messages = messagesData.data.messages; - console.log(`发现 ${messages.length} 条历史消息`); + debugLog(`发现 ${messages.length} 条历史消息`); if (messages.length > 0) { // 清空当前显示的消息 @@ -1266,15 +1314,15 @@ const appOptions = { this.scrollToBottom(); }); - console.log('历史对话内容显示完成'); + debugLog('历史对话内容显示完成'); } else { - console.log('对话存在但没有历史消息'); + debugLog('对话存在但没有历史消息'); this.logMessageState('fetchAndDisplayHistory:no-history-clear'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:no-history-cleared'); } } else { - console.log('消息数据格式不正确:', messagesData); + debugLog('消息数据格式不正确:', messagesData); this.logMessageState('fetchAndDisplayHistory:invalid-data-clear'); this.messages = []; this.logMessageState('fetchAndDisplayHistory:invalid-data-cleared'); @@ -1282,7 +1330,7 @@ const appOptions = { } catch (error) { console.error('获取历史对话失败:', error); - console.log('尝试不显示错误弹窗,仅在控制台记录'); + debugLog('尝试不显示错误弹窗,仅在控制台记录'); // 不显示alert,避免打断用户体验 this.logMessageState('fetchAndDisplayHistory:error-clear', { error: error?.message || String(error) }); this.messages = []; @@ -1297,8 +1345,8 @@ const appOptions = { // 关键功能:渲染历史消息 // ========================================== renderHistoryMessages(historyMessages) { - console.log('开始渲染历史消息...', historyMessages); - console.log('历史消息数量:', historyMessages.length); + debugLog('开始渲染历史消息...', historyMessages); + debugLog('历史消息数量:', historyMessages.length); this.logMessageState('renderHistoryMessages:start', { historyCount: historyMessages.length }); if (!Array.isArray(historyMessages)) { @@ -1309,7 +1357,7 @@ const appOptions = { let currentAssistantMessage = null; historyMessages.forEach((message, index) => { - console.log(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message); + debugLog(`处理消息 ${index + 1}/${historyMessages.length}:`, message.role, message); if (message.role === 'user') { // 用户消息 - 先结束之前的assistant消息 @@ -1322,7 +1370,7 @@ const appOptions = { role: 'user', content: message.content || '' }); - console.log('添加用户消息:', message.content?.substring(0, 50) + '...'); + debugLog('添加用户消息:', message.content?.substring(0, 50) + '...'); } else if (message.role === 'assistant') { // AI消息 - 如果没有当前assistant消息,创建一个 @@ -1333,7 +1381,9 @@ const appOptions = { streamingThinking: '', streamingText: '', currentStreamingType: null, - activeThinkingId: null + activeThinkingId: null, + awaitingFirstContent: false, + generatingLabel: '' }; } @@ -1366,7 +1416,7 @@ const appOptions = { timestamp: Date.now(), blockId }); - console.log('添加思考内容:', reasoningText.substring(0, 50) + '...'); + debugLog('添加思考内容:', reasoningText.substring(0, 50) + '...'); } // 处理普通文本内容(移除思考标签后的内容) @@ -1400,7 +1450,7 @@ const appOptions = { }, timestamp: Date.now() }); - console.log('添加append占位信息:', appendPayloadMeta.path); + debugLog('添加append占位信息:', appendPayloadMeta.path); } else if (modifyPayloadMeta) { currentAssistantMessage.actions.push({ id: `history-modify-payload-${Date.now()}-${Math.random()}`, @@ -1415,7 +1465,7 @@ const appOptions = { }, timestamp: Date.now() }); - console.log('添加modify占位信息:', modifyPayloadMeta.path); + debugLog('添加modify占位信息:', modifyPayloadMeta.path); } if (textContent && !appendPayloadMeta && !modifyPayloadMeta && !isAppendMessage && !isModifyMessage && !containsAppendMarkers) { @@ -1426,7 +1476,7 @@ const appOptions = { streaming: false, timestamp: Date.now() }); - console.log('添加文本内容:', textContent.substring(0, 50) + '...'); + debugLog('添加文本内容:', textContent.substring(0, 50) + '...'); } // 处理工具调用 @@ -1454,7 +1504,7 @@ const appOptions = { }, timestamp: Date.now() }); - console.log('添加工具调用:', toolCall.function.name); + debugLog('添加工具调用:', toolCall.function.name); }); } @@ -1507,7 +1557,7 @@ const appOptions = { if (message.name === 'append_to_file' && result && result.message) { toolAction.tool.message = result.message; } - console.log(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`); + debugLog(`更新工具结果: ${message.name} -> ${message.content?.substring(0, 50)}...`); // append_to_file 的摘要在 append_payload 占位中呈现,此处无需重复 } else { @@ -1522,7 +1572,7 @@ const appOptions = { currentAssistantMessage = null; } - console.log('处理其他类型消息:', message.role); + debugLog('处理其他类型消息:', message.role); this.messages.push({ role: message.role, content: message.content || '' @@ -1535,7 +1585,7 @@ const appOptions = { this.messages.push(currentAssistantMessage); } - console.log(`历史消息渲染完成,共 ${this.messages.length} 条消息`); + debugLog(`历史消息渲染完成,共 ${this.messages.length} 条消息`); this.logMessageState('renderHistoryMessages:after-render'); // 强制更新视图 @@ -1548,7 +1598,7 @@ const appOptions = { const blockCount = this.$el && this.$el.querySelectorAll ? this.$el.querySelectorAll('.message-block').length : 'N/A'; - console.log('[Messages] DOM 渲染统计', { + debugLog('[Messages] DOM 渲染统计', { blocks: blockCount, conversationId: this.currentConversationId }); @@ -1557,7 +1607,7 @@ const appOptions = { }, async createNewConversation() { - console.log('创建新对话...'); + debugLog('创建新对话...'); this.logMessageState('createNewConversation:start'); try { @@ -1574,7 +1624,7 @@ const appOptions = { const result = await response.json(); if (result.success) { - console.log('新对话创建成功:', result.conversation_id); + debugLog('新对话创建成功:', result.conversation_id); // 清空当前消息 this.logMessageState('createNewConversation:before-clear'); @@ -1623,7 +1673,7 @@ const appOptions = { return; } - console.log('删除对话:', conversationId); + debugLog('删除对话:', conversationId); this.logMessageState('deleteConversation:start', { conversationId }); try { @@ -1634,7 +1684,7 @@ const appOptions = { const result = await response.json(); if (result.success) { - console.log('对话删除成功'); + debugLog('对话删除成功'); // 如果删除的是当前对话,清空界面 if (conversationId === this.currentConversationId) { @@ -1671,7 +1721,7 @@ const appOptions = { }, async duplicateConversation(conversationId) { - console.log('复制对话:', conversationId); + debugLog('复制对话:', conversationId); try { const response = await fetch(`/api/conversations/${conversationId}/duplicate`, { method: 'POST' @@ -1713,7 +1763,7 @@ const appOptions = { this.searchTimer = setTimeout(() => { if (this.searchQuery.trim()) { - console.log('搜索对话:', this.searchQuery); + debugLog('搜索对话:', this.searchQuery); // TODO: 实现搜索API调用 // this.searchConversationsAPI(this.searchQuery); } else { @@ -1863,7 +1913,7 @@ const appOptions = { if (this.streamingMessage && !this.stopRequested) { this.socket.emit('stop_task'); this.stopRequested = true; - console.log('发送停止请求'); + debugLog('发送停止请求'); } }, @@ -1917,7 +1967,7 @@ const appOptions = { if (newId) { this.currentConversationId = newId; } - console.log('对话压缩完成:', result); + debugLog('对话压缩完成:', result); } else { const message = result.message || result.error || '压缩失败'; this.uiPushToast({ @@ -2080,7 +2130,7 @@ const appOptions = { enabled: !!item.enabled, tools: Array.isArray(item.tools) ? item.tools : [] })); - console.log('[ToolSettings] Snapshot applied', { + debugLog('[ToolSettings] Snapshot applied', { received: categories.length, normalized, anyEnabled: normalized.some(cat => cat.enabled), @@ -2092,23 +2142,23 @@ const appOptions = { async loadToolSettings(force = false) { if (!this.isConnected && !force) { - console.log('[ToolSettings] Skip load: disconnected & not forced'); + debugLog('[ToolSettings] Skip load: disconnected & not forced'); return; } if (this.toolSettingsLoading) { - console.log('[ToolSettings] Skip load: already loading'); + debugLog('[ToolSettings] Skip load: already loading'); return; } if (!force && this.toolSettings.length > 0) { - console.log('[ToolSettings] Skip load: already have settings'); + debugLog('[ToolSettings] Skip load: already have settings'); return; } - console.log('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected }); + debugLog('[ToolSettings] Fetch start', { force, hasConnection: this.isConnected }); this.toolSetSettingsLoading(true); try { const response = await fetch('/api/tool-settings'); const data = await response.json(); - console.log('[ToolSettings] Fetch response', { status: response.status, data }); + debugLog('[ToolSettings] Fetch response', { status: response.status, data }); if (response.ok && data.success && Array.isArray(data.categories)) { this.applyToolSettingsSnapshot(data.categories); } else { diff --git a/static/src/components/chat/ChatArea.vue b/static/src/components/chat/ChatArea.vue index 8d60989..af1edc0 100644 --- a/static/src/components/chat/ChatArea.vue +++ b/static/src/components/chat/ChatArea.vue @@ -14,6 +14,27 @@ AI Assistant
+
+
+
+ + {{ letter }} + +
+
+
) => string; }>(); +const DEFAULT_GENERATING_TEXT = '生成中…'; const rootEl = ref(null); const thinkingRefs = new Map(); @@ -203,6 +225,14 @@ function iconStyleSafe(key: string, size?: string) { return {}; } +function getGeneratingLetters(message: any) { + const label = + typeof message?.generatingLabel === 'string' && message.generatingLabel.trim() + ? message.generatingLabel.trim() + : DEFAULT_GENERATING_TEXT; + return Array.from(label); +} + defineExpose({ rootEl, getThinkingRef diff --git a/static/src/components/panels/LeftPanel.vue b/static/src/components/panels/LeftPanel.vue index 17795df..e4cdf91 100644 --- a/static/src/components/panels/LeftPanel.vue +++ b/static/src/components/panels/LeftPanel.vue @@ -11,7 +11,13 @@
- +
@@ -145,6 +151,7 @@ defineEmits<{ (event: 'toggle-panel-menu'): void; (event: 'select-panel', mode: 'files' | 'todo' | 'subAgents'): void; (event: 'open-file-manager'): void; + (event: 'toggle-thinking-mode'): void; }>(); const panelMenuWrapper = ref(null); diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 508ff33..56e4000 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -4,7 +4,15 @@ import { renderLatexInRealtime } from './useMarkdownRenderer'; export async function initializeLegacySocket(ctx: any) { try { - console.log('初始化WebSocket连接...'); + const SOCKET_DEBUG_LOGS_ENABLED = false; + const socketLog = (...args: any[]) => { + if (!SOCKET_DEBUG_LOGS_ENABLED) { + return; + } + console.log(...args); + }; + + socketLog('初始化WebSocket连接...'); const socketOptions = { transports: ['websocket', 'polling'], @@ -15,7 +23,7 @@ export async function initializeLegacySocket(ctx: any) { const STREAMING_CHAR_DELAY = 22; const STREAMING_FINALIZE_DELAY = 1000; - const STREAMING_DEBUG = true; + const STREAMING_DEBUG = false; const STREAMING_DEBUG_HISTORY_LIMIT = 2000; const streamingState = { buffer: [] as string[], @@ -169,6 +177,35 @@ export async function initializeLegacySocket(ctx: any) { } }; + const markStreamingIdleIfPossible = (source: string) => { + try { + if (!ctx) { + return; + } + if (typeof ctx.maybeResetStreamingState === 'function') { + const reset = ctx.maybeResetStreamingState(source); + if (reset) { + logStreamingDebug('streaming_idle_reset', { source }); + } + return; + } + if (!ctx.streamingMessage) { + return; + } + const hasPending = + typeof ctx.hasPendingToolActions === 'function' + ? ctx.hasPendingToolActions() + : false; + if (!hasPending) { + ctx.streamingMessage = false; + ctx.stopRequested = false; + logStreamingDebug('streaming_idle_reset:fallback', { source }); + } + } catch (error) { + console.warn('自动结束流式状态失败:', error); + } + }; + const stopStreamingTimer = () => { if (streamingState.timer !== null) { clearTimeout(streamingState.timer); @@ -247,6 +284,7 @@ export async function initializeLegacySocket(ctx: any) { logStreamingDebug('finalizeStreamingText:complete', snapshotStreamingState()); streamingState.activeMessageIndex = null; streamingState.activeTextAction = null; + markStreamingIdleIfPossible('finalizeStreamingText'); return true; }; @@ -401,7 +439,7 @@ export async function initializeLegacySocket(ctx: any) { // 连接事件 ctx.socket.on('connect', () => { ctx.isConnected = true; - console.log('WebSocket已连接'); + socketLog('WebSocket已连接'); // 连接时重置所有状态并刷新当前对话 ctx.resetAllStates('socket:connect'); scheduleHistoryReload(200); @@ -409,7 +447,7 @@ export async function initializeLegacySocket(ctx: any) { ctx.socket.on('disconnect', () => { ctx.isConnected = false; - console.log('WebSocket已断开'); + socketLog('WebSocket已断开'); // 断线时也重置状态,防止状态混乱 ctx.resetAllStates('socket:disconnect'); }); @@ -452,7 +490,7 @@ export async function initializeLegacySocket(ctx: any) { // ========================================== ctx.socket.on('token_update', (data) => { - console.log('收到token更新事件:', data); + socketLog('收到token更新事件:', data); // 只处理当前对话的token更新 if (data.conversation_id === ctx.currentConversationId) { @@ -461,7 +499,7 @@ export async function initializeLegacySocket(ctx: any) { ctx.currentConversationTokens.cumulative_output_tokens = data.cumulative_output_tokens || 0; ctx.currentConversationTokens.cumulative_total_tokens = data.cumulative_total_tokens || 0; - console.log(`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`); + socketLog(`累计Token统计更新: 输入=${data.cumulative_input_tokens}, 输出=${data.cumulative_output_tokens}, 总计=${data.cumulative_total_tokens}`); const hasContextTokens = typeof data.current_context_tokens === 'number'; if (hasContextTokens && typeof ctx.resourceSetCurrentContextTokens === 'function') { @@ -476,7 +514,7 @@ export async function initializeLegacySocket(ctx: any) { }); ctx.socket.on('todo_updated', (data) => { - console.log('收到todo更新事件:', data); + socketLog('收到todo更新事件:', data); if (data && data.conversation_id) { ctx.currentConversationId = data.conversation_id; } @@ -488,14 +526,14 @@ export async function initializeLegacySocket(ctx: any) { ctx.projectPath = data.project_path || ''; ctx.agentVersion = data.version || ctx.agentVersion; ctx.thinkingMode = !!data.thinking_mode; - console.log('系统就绪:', data); + socketLog('系统就绪:', data); // 系统就绪后立即加载对话列表 ctx.loadConversationsList(); }); ctx.socket.on('tool_settings_updated', (data) => { - console.log('收到工具设置更新:', data); + socketLog('收到工具设置更新:', data); if (data && Array.isArray(data.categories)) { ctx.applyToolSettingsSnapshot(data.categories); } @@ -507,7 +545,7 @@ export async function initializeLegacySocket(ctx: any) { // 监听对话变更事件 ctx.socket.on('conversation_changed', (data) => { - console.log('对话已切换:', data); + socketLog('对话已切换:', data); ctx.currentConversationId = data.conversation_id; ctx.currentConversationTitle = data.title || ''; ctx.promoteConversationToTop(data.conversation_id); @@ -551,10 +589,10 @@ export async function initializeLegacySocket(ctx: any) { // 监听对话加载事件 ctx.socket.on('conversation_loaded', (data) => { - console.log('对话已加载:', data); + socketLog('对话已加载:', data); if (ctx.skipConversationLoadedEvent) { ctx.skipConversationLoadedEvent = false; - console.log('跳过重复的 conversation_loaded 处理'); + socketLog('跳过重复的 conversation_loaded 处理'); return; } if (data.clear_ui) { @@ -577,7 +615,7 @@ export async function initializeLegacySocket(ctx: any) { // 监听对话列表更新事件 ctx.socket.on('conversation_list_update', (data) => { - console.log('对话列表已更新:', data); + socketLog('对话列表已更新:', data); // 刷新对话列表 ctx.loadConversationsList(); }); @@ -595,7 +633,7 @@ export async function initializeLegacySocket(ctx: any) { // AI消息开始 ctx.socket.on('ai_message_start', () => { - console.log('AI消息开始'); + socketLog('AI消息开始'); logStreamingDebug('socket:ai_message_start'); finalizeStreamingText({ force: true }); resetStreamingBuffer(); @@ -612,7 +650,7 @@ export async function initializeLegacySocket(ctx: any) { // 思考流开始 ctx.socket.on('thinking_start', () => { - console.log('思考开始'); + socketLog('思考开始'); const result = ctx.chatStartThinkingAction(); if (result && result.blockId) { const blockId = result.blockId; @@ -641,7 +679,7 @@ export async function initializeLegacySocket(ctx: any) { // 思考结束 ctx.socket.on('thinking_end', (data) => { - console.log('思考结束'); + socketLog('思考结束'); const blockId = ctx.chatCompleteThinkingAction(data.full_content); if (blockId) { setTimeout(() => { @@ -656,7 +694,7 @@ export async function initializeLegacySocket(ctx: any) { // 文本流开始 ctx.socket.on('text_start', () => { - console.log('文本开始'); + socketLog('文本开始'); logStreamingDebug('socket:text_start'); finalizeStreamingText({ force: true }); resetStreamingBuffer(); @@ -694,7 +732,7 @@ export async function initializeLegacySocket(ctx: any) { // 文本结束 ctx.socket.on('text_end', (data) => { - console.log('文本结束'); + socketLog('文本结束'); logStreamingDebug('socket:text_end', { finalLength: (data?.full_content || '').length, snapshot: snapshotStreamingState() @@ -710,13 +748,13 @@ export async function initializeLegacySocket(ctx: any) { // 工具提示事件(可选) ctx.socket.on('tool_hint', (data) => { - console.log('工具提示:', data.name); + socketLog('工具提示:', data.name); // 可以在这里添加提示UI }); // 工具准备中事件 - 实时显示 ctx.socket.on('tool_preparing', (data) => { - console.log('工具准备中:', data.name); + socketLog('工具准备中:', data.name); const msg = ctx.chatEnsureAssistantMessage(); if (!msg) { return; @@ -746,7 +784,7 @@ export async function initializeLegacySocket(ctx: any) { // 工具状态更新事件 - 实时显示详细状态 ctx.socket.on('tool_status', (data) => { - console.log('工具状态:', data); + socketLog('工具状态:', data); const target = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id); if (target) { target.tool.statusDetail = data.detail; @@ -765,7 +803,7 @@ export async function initializeLegacySocket(ctx: any) { // 工具开始(从准备转为执行) ctx.socket.on('tool_start', (data) => { - console.log('工具开始执行:', data.name); + socketLog('工具开始执行:', data.name); let action = null; if (data.preparing_id && ctx.preparingTools.has(data.preparing_id)) { action = ctx.preparingTools.get(data.preparing_id); @@ -809,7 +847,7 @@ export async function initializeLegacySocket(ctx: any) { // 更新action(工具完成) ctx.socket.on('update_action', (data) => { - console.log('更新action:', data.id, 'status:', data.status); + socketLog('更新action:', data.id, 'status:', data.status); let targetAction = ctx.toolFindAction(data.id, data.preparing_id, data.execution_id); if (!targetAction && data.preparing_id && ctx.preparingTools.has(data.preparing_id)) { targetAction = ctx.preparingTools.get(data.preparing_id); @@ -872,18 +910,19 @@ export async function initializeLegacySocket(ctx: any) { } ctx.$forceUpdate(); ctx.conditionalScrollToBottom(); + markStreamingIdleIfPossible('update_action'); } // 关键修复:每个工具完成后都更新当前上下文Token - if (data.status === 'completed') { - setTimeout(() => { - ctx.updateCurrentContextTokens(); - }, 500); - } - }); + if (data.status === 'completed') { + setTimeout(() => { + ctx.updateCurrentContextTokens(); + }, 500); + } + }); ctx.socket.on('append_payload', (data) => { - console.log('收到append_payload事件:', data); + socketLog('收到append_payload事件:', data); ctx.chatAddAppendPayloadAction({ path: data.path || '未知文件', forced: !!data.forced, @@ -896,7 +935,7 @@ export async function initializeLegacySocket(ctx: any) { }); ctx.socket.on('modify_payload', (data) => { - console.log('收到modify_payload事件:', data); + socketLog('收到modify_payload事件:', data); ctx.chatAddModifyPayloadAction({ path: data.path || '未知文件', total: data.total ?? null, @@ -910,19 +949,19 @@ export async function initializeLegacySocket(ctx: any) { // 停止请求确认 ctx.socket.on('stop_requested', (data) => { - console.log('停止请求已接收:', data.message); + socketLog('停止请求已接收:', data.message); // 可以显示提示信息 }); // 任务停止 ctx.socket.on('task_stopped', (data) => { - console.log('任务已停止:', data.message); + socketLog('任务已停止:', data.message); ctx.resetAllStates('socket:task_stopped'); }); // 任务完成(重点:更新Token统计) ctx.socket.on('task_complete', (data) => { - console.log('任务完成', data); + socketLog('任务完成', data); ctx.resetAllStates('socket:task_complete'); // 任务完成后立即更新Token统计(关键修复) diff --git a/static/src/stores/chat.ts b/static/src/stores/chat.ts index 6f2bc31..3462311 100644 --- a/static/src/stores/chat.ts +++ b/static/src/stores/chat.ts @@ -15,6 +15,31 @@ interface ChatState { thinkingScrollLocks: Map; } +const GENERATING_LABELS = [ + '正在构思…', + '稍候,AI 正在准备', + '准备工具中', + '容我三思…', + '答案马上就来', + '灵感加载中', + '思路拼装中', + '琢磨最佳方案', + '脑内开会中', + '整理资料中', + '润色回复中', + '调配上下文', + '搜刮记忆中', + '快敲完了,别急' +]; + +function randomGeneratingLabel() { + if (!GENERATING_LABELS.length) { + return ''; + } + const index = Math.floor(Math.random() * GENERATING_LABELS.length); + return GENERATING_LABELS[index]; +} + function createAssistantMessage() { return { role: 'assistant', @@ -22,7 +47,9 @@ function createAssistantMessage() { streamingThinking: '', streamingText: '', currentStreamingType: null, - activeThinkingId: null + activeThinkingId: null, + awaitingFirstContent: false, + generatingLabel: randomGeneratingLabel() }; } @@ -38,6 +65,12 @@ function cloneMap(source: Map) { return new Map(Array.from(source.entries())); } +function clearAwaitingFirstContent(message: any) { + if (message && message.awaitingFirstContent) { + message.awaitingFirstContent = false; + } +} + export const useChatStore = defineStore('chat', { state: (): ChatState => ({ messages: [], @@ -142,10 +175,12 @@ export const useChatStore = defineStore('chat', { this.messages.push(message); this.currentMessageIndex = this.messages.length - 1; this.streamingMessage = true; + message.awaitingFirstContent = true; return message; }, startThinkingAction() { const msg = this.ensureAssistantMessage(); + clearAwaitingFirstContent(msg); msg.streamingThinking = ''; msg.currentStreamingType = 'thinking'; const actionId = randomId('thinking'); @@ -192,6 +227,7 @@ export const useChatStore = defineStore('chat', { if (!msg) { return null; } + clearAwaitingFirstContent(msg); msg.streamingText = ''; msg.currentStreamingType = 'text'; const action = { @@ -237,6 +273,7 @@ export const useChatStore = defineStore('chat', { }, addSystemMessage(content: string) { const msg = this.ensureAssistantMessage(); + clearAwaitingFirstContent(msg); msg.actions.push({ id: randomId('system'), type: 'system', @@ -246,6 +283,7 @@ export const useChatStore = defineStore('chat', { }, addAppendPayloadAction(data: any) { const msg = this.ensureAssistantMessage(); + clearAwaitingFirstContent(msg); msg.actions.push({ id: `append-payload-${Date.now()}-${Math.random()}`, type: 'append_payload', @@ -255,6 +293,7 @@ export const useChatStore = defineStore('chat', { }, addModifyPayloadAction(data: any) { const msg = this.ensureAssistantMessage(); + clearAwaitingFirstContent(msg); msg.actions.push({ id: `modify-payload-${Date.now()}-${Math.random()}`, type: 'modify_payload', diff --git a/static/src/stores/file.ts b/static/src/stores/file.ts index fe11752..d696c56 100644 --- a/static/src/stores/file.ts +++ b/static/src/stores/file.ts @@ -1,5 +1,13 @@ import { defineStore } from 'pinia'; +const FILE_STORE_DEBUG_LOGS = false; +function fileDebugLog(...args: unknown[]) { + if (!FILE_STORE_DEBUG_LOGS) { + return; + } + console.log(...args); +} + interface FileNode { type: 'folder' | 'file'; name: string; @@ -80,7 +88,7 @@ export const useFileStore = defineStore('file', { try { const response = await fetch('/api/files'); const data = await response.json(); - console.log('[FileTree] fetch result', data); + fileDebugLog('[FileTree] fetch result', data); this.setFileTreeFromResponse(data); } catch (error) { console.error('获取文件树失败:', error); @@ -127,7 +135,7 @@ export const useFileStore = defineStore('file', { return; } const current = !!this.expandedFolders[path]; - console.log('[FileTree] toggle folder', path, '=>', !current); + fileDebugLog('[FileTree] toggle folder', path, '=>', !current); this.expandedFolders = { ...this.expandedFolders, [path]: !current diff --git a/static/src/styles/components/chat/_chat-area.scss b/static/src/styles/components/chat/_chat-area.scss index ec913d8..c5e1e72 100644 --- a/static/src/styles/components/chat/_chat-area.scss +++ b/static/src/styles/components/chat/_chat-area.scss @@ -147,6 +147,49 @@ border-left: 4px solid var(--claude-accent); } +.assistant-generating-block { + width: 100%; +} + +.text-content.assistant-generating-placeholder { + display: flex; + width: 100%; + align-items: center; + gap: 0.08em; + padding: 8px 0 16px; + margin: 0; + font-size: 15px; + font-weight: 600; + color: var(--claude-text); + letter-spacing: 0.08em; + background: transparent; + border: none; + box-shadow: none; +} + +.assistant-generating-letter { + display: inline-block; + opacity: 0.35; + transform: translateY(0); + animation: assistant-generating-wave 1.6s ease-in-out infinite; +} + +@keyframes assistant-generating-wave { + 0%, + 100% { + opacity: 0.35; + transform: translateY(0); + } + 20% { + opacity: 1; + transform: translateY(-3px) scale(1.05); + } + 40% { + opacity: 0.65; + transform: translateY(0); + } +} + .thinking-content { white-space: pre-wrap; word-wrap: break-word; @@ -322,6 +365,34 @@ padding: 0 20px 0 15px; } +.text-output .text-content table { + width: 100%; + border-collapse: collapse; + margin: 16px 0; + background: rgba(255, 255, 255, 0.92); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 8px 20px rgba(61, 57, 41, 0.06); +} + +.text-output .text-content thead { + background: rgba(218, 119, 86, 0.1); +} + +.text-output .text-content th, +.text-output .text-content td { + border: 1px solid rgba(118, 103, 84, 0.18); + padding: 10px 14px; + text-align: left; + vertical-align: middle; + font-size: 14px; +} + +.text-output .text-content th { + font-weight: 600; + color: var(--claude-text); +} + .system-action { margin: 12px 0; padding: 10px 14px; diff --git a/static/src/styles/components/panels/_left-panel.scss b/static/src/styles/components/panels/_left-panel.scss index 568c149..8c62142 100644 --- a/static/src/styles/components/panels/_left-panel.scss +++ b/static/src/styles/components/panels/_left-panel.scss @@ -108,6 +108,10 @@ } .mode-indicator { + border: none; + cursor: pointer; + outline: none; + padding: 0; width: 36px; height: 36px; border-radius: 18px; @@ -120,6 +124,14 @@ transition: background 0.25s ease, box-shadow 0.25s ease, transform 0.25s ease; } +.mode-indicator:hover { + transform: translateY(-1px); +} + +.mode-indicator:focus-visible { + box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 8px 20px rgba(189, 93, 58, 0.35); +} + .mode-indicator.fast { background: #ffcc4d; box-shadow: 0 8px 20px rgba(255, 204, 77, 0.35);