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
+
) => 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);