feat: improve ui feedback

This commit is contained in:
JOJO 2025-11-30 00:09:05 +08:00
parent 09654b7d4b
commit 87ceaad92b
9 changed files with 346 additions and 89 deletions

View File

@ -69,6 +69,7 @@
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
@toggle-thinking-mode="handleQuickModeToggle"
/>
<div

View File

@ -96,6 +96,14 @@ if (window.visualViewport) {
window.visualViewport.addEventListener('scroll', updateViewportHeightVar);
}
const ENABLE_APP_DEBUG_LOGS = false;
function debugLog(...args) {
if (!ENABLE_APP_DEBUG_LOGS) {
return;
}
debugLog(...args);
}
const appOptions = {
data() {
return {
@ -149,7 +157,7 @@ const appOptions = {
},
async mounted() {
console.log('Vue应用已挂载');
debugLog('Vue应用已挂载');
if (window.ensureCsrfToken) {
window.ensureCsrfToken().catch((err) => {
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 {

View File

@ -14,6 +14,27 @@
<span class="icon icon-sm" :style="iconStyleSafe('bot')" aria-hidden="true"></span>
<span>AI Assistant</span>
</div>
<div
v-if="msg.awaitingFirstContent"
class="action-item streaming-content immediate-show assistant-generating-block"
>
<div class="text-output">
<div
class="text-content assistant-generating-placeholder"
role="status"
aria-live="polite"
>
<span
v-for="(letter, letterIndex) in getGeneratingLetters(msg)"
:key="letterIndex"
class="assistant-generating-letter"
:style="{ animationDelay: `${letterIndex * 0.08}s` }"
>
{{ letter }}
</span>
</div>
</div>
</div>
<div
v-for="(action, actionIndex) in msg.actions || []"
:key="action.id || `${index}-${actionIndex}`"
@ -181,6 +202,7 @@ const props = defineProps<{
formatSearchTime: (filters: Record<string, any>) => string;
}>();
const DEFAULT_GENERATING_TEXT = '生成中…';
const rootEl = ref<HTMLElement | null>(null);
const thinkingRefs = new Map<string, HTMLElement | null>();
@ -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

View File

@ -11,7 +11,13 @@
</div>
</div>
<div class="status-indicators">
<span class="mode-indicator" :class="{ thinking: thinkingMode, fast: !thinkingMode }">
<button
type="button"
class="mode-indicator"
:class="{ thinking: thinkingMode, fast: !thinkingMode }"
:title="thinkingMode ? '思考模式(点击切换)' : '快速模式(点击切换)'"
@click="$emit('toggle-thinking-mode')"
>
<transition name="mode-icon" mode="out-in">
<span
class="icon icon-sm"
@ -20,7 +26,7 @@
aria-hidden="true"
></span>
</transition>
</span>
</button>
<span class="connection-dot" :class="{ active: isConnected }" :title="isConnected ? '已连接' : '未连接'"></span>
</div>
</div>
@ -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<HTMLElement | null>(null);

View File

@ -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,6 +910,7 @@ export async function initializeLegacySocket(ctx: any) {
}
ctx.$forceUpdate();
ctx.conditionalScrollToBottom();
markStreamingIdleIfPossible('update_action');
}
// 关键修复每个工具完成后都更新当前上下文Token
@ -883,7 +922,7 @@ export async function initializeLegacySocket(ctx: any) {
});
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统计关键修复

View File

@ -15,6 +15,31 @@ interface ChatState {
thinkingScrollLocks: Map<string, boolean>;
}
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<K, V>(source: Map<K, V>) {
return new Map<K, V>(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',

View File

@ -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

View File

@ -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;

View File

@ -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);