feat: improve ui feedback
This commit is contained in:
parent
09654b7d4b
commit
87ceaad92b
@ -69,6 +69,7 @@
|
||||
@toggle-panel-menu="togglePanelMenu"
|
||||
@select-panel="selectPanelMode"
|
||||
@open-file-manager="openGuiFileManager"
|
||||
@toggle-thinking-mode="handleQuickModeToggle"
|
||||
/>
|
||||
|
||||
<div
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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统计(关键修复)
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user