fix: integrate sidebar components

This commit is contained in:
JOJO 2025-11-25 23:19:38 +08:00
parent 7cc91571de
commit 2d083786bf
4 changed files with 181 additions and 666 deletions

View File

@ -1,665 +1,150 @@
<template>
<div id="app">
<transition name="quota-toast-fade">
<div class="quota-toast" v-if="quotaToast">
<span class="quota-toast-label">{{ quotaToast.message }}</span>
<button type="button"
class="toast-close"
aria-label="关闭通知"
@click="dismissQuotaToast">
×
</button>
</div>
</transition>
<div class="toast-stack" :class="{ 'toast-stack--empty': toastQueue.length === 0 }">
<transition-group name="quota-toast-fade" tag="div">
<div class="app-toast"
v-for="toast in toastQueue"
:key="toast.id"
:class="['app-toast', toast.type ? `app-toast--${toast.type}` : '']">
<div class="app-toast-body">
<div v-if="toast.title" class="app-toast-title">{{ toast.title }}</div>
<div class="app-toast-message">{{ toast.message }}</div>
</div>
<button v-if="toast.closable !== false"
type="button"
class="toast-close"
@click="dismissToast(toast.id)">
×
</button>
</div>
</transition-group>
</div>
<div class="confirm-overlay"
v-if="confirmDialog && confirmDialog.visible"
@click.self="handleConfirm(false)">
<div class="confirm-modal">
<div class="confirm-title">{{ confirmDialog.title || '确认操作' }}</div>
<div class="confirm-message">{{ confirmDialog.message }}</div>
<div class="confirm-actions">
<button type="button" class="confirm-button" @click="handleConfirm(false)">
{{ confirmDialog.cancelText || '取消' }}
</button>
<button type="button"
class="confirm-button confirm-button--primary"
@click="handleConfirm(true)">
{{ confirmDialog.confirmText || '确认' }}
</button>
</div>
</div>
</div>
<!-- Loading indicator -->
<div v-if="!isConnected && messages.length === 0" style="text-align: center; padding: 50px;">
<h2>正在连接服务器...</h2>
<p>如果长时间无响应请刷新页面</p>
</div>
<!-- Main UI (只在连接后显示) -->
<template v-else>
<div class="main-container">
<!-- 新增对话历史侧边栏最左侧 -->
<aside class="conversation-sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="conversation-header" :class="{ 'collapsed-layout': sidebarCollapsed }">
<template v-if="sidebarCollapsed">
<div class="collapsed-header-buttons">
<button type="button"
class="collapsed-control-btn conversation-menu-btn"
title="展开对话记录"
@click="toggleSidebar">
<span class="sr-only">展开对话记录</span>
<span class="chat-icon" aria-hidden="true">
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"/>
<path d="M9 9.5h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
<path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>
</svg>
</span>
</button>
<button type="button"
class="collapsed-control-btn quick-plus-btn"
title="快捷新建对话"
@click="createNewConversation">
<span class="sr-only">新建对话</span>
<span class="pencil-icon" aria-hidden="true">
<svg class="edit-svgIcon" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1v32c0 8.8 7.2 16 16 16h32zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"></path>
</svg>
</span>
</button>
</div>
</template>
<template v-else>
<button @click="createNewConversation" class="new-conversation-btn">
<span class="btn-icon">+</span>
<span class="btn-text">新建对话</span>
</button>
<button @click="toggleSidebar" class="toggle-sidebar-btn">
<span v-if="sidebarCollapsed"
class="icon icon-md"
:style="iconStyle('menu')"
aria-hidden="true"></span>
<span v-else class="toggle-arrow" aria-hidden="true"></span>
</button>
</template>
</div>
<template v-if="!sidebarCollapsed">
<div class="conversation-search">
<input v-model="searchQuery"
@input="searchConversations"
placeholder="搜索对话..."
class="search-input">
</div>
<div class="conversation-list">
<div v-if="conversationsLoading" class="loading-conversations">
正在加载...
</div>
<div v-else-if="conversations.length === 0" class="no-conversations">
暂无对话记录
</div>
<div v-else>
<div v-for="conv in conversations"
:key="conv.id"
class="conversation-item"
:class="{ active: conv.id === currentConversationId }"
@click="loadConversation(conv.id)">
<div class="conversation-title">{{ conv.title }}</div>
<div class="conversation-meta">
<span class="conversation-time">{{ formatTime(conv.updated_at) }}</span>
<span class="conversation-counts">
{{ conv.total_messages }}条消息
<span v-if="conv.total_tools > 0"> · {{ conv.total_tools }}工具</span>
</span>
</div>
<div class="conversation-actions" @click.stop>
<button @click="deleteConversation(conv.id)"
class="conversation-action-btn delete-btn"
title="删除对话">
×
</button>
<button @click="duplicateConversation(conv.id)"
class="conversation-action-btn copy-btn"
title="复制对话">
</button>
</div>
</div>
</div>
<div v-if="hasMoreConversations" class="load-more">
<button @click="loadMoreConversations"
:disabled="loadingMoreConversations"
class="load-more-btn">
{{ loadingMoreConversations ? '载入中...' : '加载更多' }}
</button>
</div>
</div>
</template>
<template v-else>
<div class="conversation-collapsed-spacer"></div>
</template>
<div class="conversation-personal-entry" :class="{ collapsed: sidebarCollapsed }">
<button type="button"
class="personal-page-btn"
:class="{ 'icon-only': sidebarCollapsed }"
title="个人页面"
@click="openPersonalPage">
<span class="sr-only">个人页面</span>
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true">
<path fill-rule="evenodd"
clip-rule="evenodd"
d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"></path>
</svg>
<span class="personal-label" v-if="!sidebarCollapsed">个人页面</span>
</button>
</div>
</aside>
<!-- 左侧文件树 -->
<aside class="sidebar left-sidebar" :style="{ width: leftWidth + 'px' }">
<div class="sidebar-status">
<div class="compact-status-card">
<div class="status-line">
<div class="status-brand">
<span class="icon icon-lg status-logo"
:style="iconStyle('bot')"
aria-hidden="true"></span>
<div class="brand-text">
<span class="brand-name">AI Agent</span>
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
</div>
</div>
<div class="status-indicators">
<span class="mode-indicator"
:class="{ thinking: thinkingMode, fast: !thinkingMode }">
<transition name="mode-icon" mode="out-in">
<span class="icon icon-sm"
:style="iconStyle(thinkingMode ? 'brain' : 'zap')"
:key="thinkingMode ? 'brain' : 'zap'"
aria-hidden="true"></span>
</transition>
</span>
<span class="connection-dot"
:class="{ active: isConnected }"
:title="isConnected ? '已连接' : '未连接'"></span>
</div>
</div>
</div>
</div>
<div class="sidebar-panel-card-wrapper">
<div class="sidebar-panel-card">
<div class="sidebar-header">
<div class="panel-menu-wrapper" ref="panelMenuWrapper">
<button class="sidebar-view-toggle"
@click.stop="togglePanelMenu"
title="切换侧边栏">
<span class="icon icon-md"
:style="iconStyle('menu')"
aria-hidden="true"></span>
</button>
<transition name="fade">
<div class="panel-menu" v-if="panelMenuOpen">
<button type="button"
:class="{ active: panelMode === 'files' }"
@click.stop="selectPanelMode('files')"
title="项目文件">
<span class="icon icon-md"
:style="iconStyle('folder')"
aria-hidden="true"></span>
</button>
<button type="button"
:class="{ active: panelMode === 'todo' }"
@click.stop="selectPanelMode('todo')"
title="待办列表">
<span class="icon icon-md"
:style="iconStyle('stickyNote')"
aria-hidden="true"></span>
</button>
<button type="button"
:class="{ active: panelMode === 'subAgents' }"
@click.stop="selectPanelMode('subAgents')"
title="子智能体">
<span class="icon icon-md"
:style="iconStyle('bot')"
aria-hidden="true"></span>
</button>
</div>
</transition>
</div>
<button class="sidebar-manage-btn"
@click="openGuiFileManager"
title="打开桌面式文件管理器">
管理
</button>
<h3>
<span v-if="panelMode === 'files'" class="icon-label">
<span class="icon icon-sm"
:style="iconStyle('folder')"
aria-hidden="true"></span>
<span>项目文件</span>
</span>
<span v-else-if="panelMode === 'todo'" class="icon-label">
<span class="icon icon-sm"
:style="iconStyle('stickyNote')"
aria-hidden="true"></span>
<span>待办列表</span>
</span>
<span v-else class="icon-label">
<span class="icon icon-sm"
:style="iconStyle('bot')"
aria-hidden="true"></span>
<span>子智能体</span>
</span>
</h3>
</div>
<div class="sidebar-panel-content">
<div v-if="panelMode === 'todo'" class="todo-panel">
<div v-if="!todoList" class="todo-empty">
暂无待办列表
</div>
<div v-else>
<div class="todo-task"
v-for="task in (todoList.tasks || [])"
:key="task.index"
:class="{ done: task.status === 'done' }">
<span class="todo-task-title">task{{ task.index }}{{ task.title }}</span>
<span class="todo-task-status icon-label">
<span class="icon icon-sm"
:style="iconStyle(task.status === 'done' ? 'check' : 'checkbox')"
aria-hidden="true"></span>
<span>{{ task.status === 'done' ? '完成' : '未完成' }}</span>
</span>
</div>
<div class="todo-instruction">{{ todoList.instruction }}</div>
</div>
</div>
<div v-else-if="panelMode === 'subAgents'" class="sub-agent-panel">
<div v-if="!subAgents.length" class="sub-agent-empty">
暂无运行中的子智能体
</div>
<div v-else class="sub-agent-cards">
<div class="sub-agent-card"
v-for="agent in subAgents"
:key="agent.task_id"
@click="openSubAgent(agent)">
<div class="sub-agent-header">
<span class="sub-agent-id">#{{ agent.agent_id }}</span>
<span class="sub-agent-status" :class="agent.status">{{ agent.status }}</span>
</div>
<div class="sub-agent-summary">{{ agent.summary }}</div>
<div class="sub-agent-tool" v-if="agent.last_tool">
当前{{ agent.last_tool }}
</div>
</div>
</div>
</div>
<div v-else class="file-tree" @contextmenu.prevent>
<file-node
v-for="node in fileTree"
:key="node.path"
:node="node"
:level="0"
:expanded-folders="expandedFolders"
:icon-style="iconStyle"
@toggle-folder="toggleFolder"
@context-menu="showContextMenu"
></file-node>
</div>
</div>
</div>
</div>
</aside>
<!-- 左侧拖拽手柄 -->
<div class="resize-handle" @mousedown="startResize('left', $event)"></div>
<!-- 中间聊天区域 -->
<main class="chat-container">
<div class="token-drawer" v-if="currentConversationId" :class="{ collapsed: tokenPanelCollapsed }">
<div class="token-display-panel">
<div class="token-panel-content">
<div class="usage-dashboard">
<div class="usage-cell usage-cell--left usage-cell--token panel-card">
<div class="usage-title">Token 统计</div>
<div class="stat-grid stat-grid--triple">
<div class="stat-block">
<div class="stat-label">当前上下文</div>
<div class="stat-value stat-value--accent">{{ formatTokenCount(currentContextTokens || 0) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">累计输入</div>
<div class="stat-value stat-value--success">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">累计输出</div>
<div class="stat-value stat-value--warning">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</div>
</div>
</div>
</div>
<div class="usage-cell usage-cell--right usage-cell--performance panel-card">
<div class="usage-title">
<span>性能统计</span>
<span class="status-pill" v-if="containerStatus" :class="containerStatusClass()">{{ containerStatusText() }}</span>
</div>
<template v-if="hasContainerStats()">
<div class="stat-grid stat-grid--double">
<div class="stat-block">
<div class="stat-label">CPU</div>
<div class="stat-value">{{ formatPercentage(containerStatus.stats.cpu_percent) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">内存</div>
<div class="stat-value stat-value--mono">
{{ formatBytes(containerStatus.stats.memory.used_bytes) }}
<template v-if="containerStatus.stats.memory.limit_bytes">
/ {{ formatBytes(containerStatus.stats.memory.limit_bytes) }}
</template>
</div>
<div class="stat-foot" v-if="containerStatus.stats.memory.percent">
{{ formatPercentage(containerStatus.stats.memory.percent) }}
</div>
</div>
</div>
</template>
<div class="usage-placeholder" v-else>
当前运行在宿主机模式暂无容器指标
</div>
</div>
<div class="usage-cell usage-cell--left usage-cell--quota panel-card">
<div class="usage-title">额度统计</div>
<div class="stat-grid stat-grid--triple">
<div class="stat-block">
<div class="stat-label">常规模型</div>
<div class="stat-value">{{ formatQuotaValue(usageQuota.fast) }}</div>
<div class="stat-foot" v-if="(usageQuota.fast.count || 0) > 0">重置 {{ formatResetTime(usageQuota.fast.reset_at) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">思考模型</div>
<div class="stat-value">{{ formatQuotaValue(usageQuota.thinking) }}</div>
<div class="stat-foot" v-if="(usageQuota.thinking.count || 0) > 0">重置 {{ formatResetTime(usageQuota.thinking.reset_at) }}</div>
</div>
<div class="stat-block">
<div class="stat-label">搜索</div>
<div class="stat-value">{{ formatQuotaValue(usageQuota.search) }}</div>
<div class="stat-foot" v-if="(usageQuota.search.count || 0) > 0">重置 {{ formatResetTime(usageQuota.search.reset_at) }}</div>
</div>
</div>
</div>
<div class="usage-cell usage-cell--right usage-cell--resource panel-card">
<div class="usage-title">资源统计</div>
<div class="stat-grid stat-grid--double">
<div class="stat-block">
<div class="stat-label">网络</div>
<div class="stat-value stat-value--mono">
{{ formatRate(containerNetRate.down_bps) }}
{{ formatRate(containerNetRate.up_bps) }}
</div>
</div>
<div class="stat-block">
<div class="stat-label">存储</div>
<div class="stat-value stat-value--mono">
{{ formatBytes(projectStorage.used_bytes) }}
<template v-if="projectStorage.limit_bytes">
/ {{ formatBytes(projectStorage.limit_bytes) }}
</template>
</div>
<div class="stat-foot" v-if="typeof projectStorage.usage_percent === 'number'">
{{ projectStorage.usage_percent.toFixed(1) }}%
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ChatArea
ref="messagesArea"
:messages="messages"
:icon-style="iconStyle"
:expanded-blocks="expandedBlocks"
:render-markdown="renderMarkdown"
:toggle-block="toggleBlock"
:handle-thinking-scroll="handleThinkingScroll"
:streaming-message="streamingMessage"
:get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText"
:get-tool-animation-class="getToolAnimationClass"
:get-tool-description="getToolDescription"
:format-search-topic="formatSearchTopic"
:format-search-time="formatSearchTime"
/>
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
<button @click="toggleScrollLock" class="scroll-lock-btn">
<svg v-if="autoScrollEnabled && !userScrolling" viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
</svg>
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v12" />
<path d="M7 13l5 5 5-5" />
</svg>
</button>
</div>
<!-- 输入区域 -->
<div class="input-area compact-input-area">
<div class="stadium-input-wrapper" ref="stadiumShellOuter">
<div
class="stadium-shell"
ref="compactInputShell"
:class="{
'is-multiline': inputIsMultiline,
'is-focused': inputIsFocused,
'has-text': inputMessage.trim().length > 0
}">
<input type="file"
ref="fileUploadInput"
class="file-input-hidden"
@change="handleFileSelected">
<button type="button"
class="stadium-btn add-btn"
@click.stop="toggleQuickMenu"
:disabled="!isConnected">
+
</button>
<textarea
ref="stadiumInput"
v-model="inputMessage"
@input="handleInputChange"
@focus="handleInputFocus"
@blur="handleInputBlur"
@keydown.enter.ctrl="sendMessage"
placeholder="输入消息... (Ctrl+Enter 发送)"
class="stadium-input"
:disabled="!isConnected || streamingMessage"
rows="1">
</textarea>
<button type="button"
class="stadium-btn send-btn"
@click="handleSendOrStop"
:disabled="!isConnected || (!inputMessage.trim() && !streamingMessage)">
<span v-if="streamingMessage" class="stop-icon"></span>
<span v-else class="send-icon"></span>
</button>
</div>
<transition name="quick-menu">
<div class="quick-menu" v-if="quickMenuOpen" ref="quickMenu" @click.stop>
<button type="button"
class="menu-entry"
@click="handleQuickUpload"
:disabled="!isConnected || uploading">
{{ uploading ? '上传中...' : '上传文件' }}
</button>
<button type="button"
class="menu-entry has-submenu"
@click.stop="toggleToolMenu"
:disabled="!isConnected">
工具禁用
<span class="entry-arrow"></span>
</button>
<button type="button"
class="menu-entry"
@click="handleQuickModeToggle"
:disabled="streamingMessage || !isConnected">
{{ thinkingMode ? '快速模式' : '思考模式' }}
</button>
<button type="button"
class="menu-entry has-submenu"
@click.stop="toggleSettings"
:disabled="!isConnected">
设置
<span class="entry-arrow"></span>
</button>
<transition name="submenu-slide">
<div class="quick-submenu tool-submenu" v-if="toolMenuOpen">
<div class="submenu-status" v-if="toolSettingsLoading">
正在同步工具状态...
</div>
<div v-else-if="toolSettings.length === 0" class="submenu-empty">
暂无可控工具
</div>
<div v-else class="submenu-list">
<button v-for="category in toolSettings"
:key="category.id"
type="button"
class="menu-entry submenu-entry"
:class="{ disabled: !category.enabled }"
@click.stop="updateToolCategory(category.id, !category.enabled)"
:disabled="streamingMessage || !isConnected || toolSettingsLoading">
<span class="submenu-label icon-label">
<span class="icon icon-sm"
:style="iconStyle(toolCategoryIcon(category.id))"
aria-hidden="true"></span>
<span>{{ category.label }}</span>
</span>
<span class="entry-arrow">{{ category.enabled ? '禁用' : '启用' }}</span>
</button>
</div>
</div>
</transition>
<transition name="submenu-slide">
<div class="quick-submenu settings-submenu" v-if="settingsOpen">
<div class="submenu-list">
<button type="button"
class="menu-entry submenu-entry"
@click="handleRealtimeTerminalClick"
:disabled="!isConnected">
实时终端
</button>
<button type="button"
class="menu-entry submenu-entry"
@click="handleFocusPanelToggleClick"
:disabled="!isConnected">
聚焦面板
</button>
<button type="button"
class="menu-entry submenu-entry"
@click="handleTokenPanelToggleClick"
:disabled="!currentConversationId">
用量统计
</button>
<button type="button"
class="menu-entry submenu-entry"
@click="handleCompressConversationClick"
:disabled="compressing || streamingMessage || !isConnected">
{{ compressing ? '压缩中...' : '压缩对话' }}
</button>
</div>
</div>
</transition>
</div>
</transition>
</div>
</div>
</main>
<!-- 右侧拖拽手柄 -->
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
<!-- 右侧聚焦文件 -->
<aside class="sidebar right-sidebar"
:class="{ collapsed: rightCollapsed }"
:style="{ width: rightCollapsed ? '0px' : rightWidth + 'px' }">
<div class="sidebar-header">
<h3 class="icon-label">
<span class="icon icon-sm"
:style="iconStyle('eye')"
aria-hidden="true"></span>
<span>聚焦文件 ({{ Object.keys(focusedFiles).length }}/3)</span>
</h3>
</div>
<div class="focused-files" v-if="!rightCollapsed">
<div v-if="Object.keys(focusedFiles).length === 0" class="no-files">
暂无聚焦文件
</div>
<div v-else class="file-tabs">
<div v-for="(file, path) in focusedFiles" :key="path" class="file-tab">
<div class="tab-header">
<span class="file-name">{{ path.split('/').pop() }}</span>
<span class="file-size">{{ (file.size / 1024).toFixed(1) }}KB</span>
</div>
<div class="file-content">
<pre><code :class="getLanguageClass(path)">{{ file.content }}</code></pre>
</div>
</div>
</div>
</div>
</aside>
</div>
<PersonalizationDrawer />
</template>
<div class="easter-egg-overlay"
v-show="easterEgg.active"
:class="{ active: easterEgg.active }"
aria-hidden="true"
ref="easterEggRoot">
</div>
<div class="context-menu"
v-if="contextMenu.visible"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }"
@click.stop>
<button v-if="contextMenu.node && contextMenu.node.type === 'file'"
@click.stop="downloadFile(contextMenu.node.path)">
下载文件
</button>
<button v-if="contextMenu.node && contextMenu.node.type === 'folder'"
:disabled="!contextMenu.node.path"
@click.stop="downloadFolder(contextMenu.node.path)">
下载压缩包
</button>
</div>
<AppShell :download-file="downloadFile" :download-folder="downloadFolder">
<div
v-if="!isConnected && messages.length === 0"
class="app-loading-state"
style="text-align: center; padding: 50px;"
>
<h2>正在连接服务器...</h2>
<p>如果长时间无响应请刷新页面</p>
</div>
<template v-else>
<div class="main-container">
<ConversationSidebar
:icon-style="iconStyle"
:format-time="formatTime"
@toggle="toggleSidebar"
@create="createNewConversation"
@search="handleSidebarSearch"
@select="loadConversation"
@load-more="loadMoreConversations"
@personal="openPersonalPage"
@delete="deleteConversation"
@duplicate="duplicateConversation"
/>
<LeftPanel
ref="leftPanel"
:width="leftWidth"
:icon-style="iconStyle"
:agent-version="agentVersion"
:thinking-mode="thinkingMode"
:is-connected="isConnected"
:panel-menu-open="panelMenuOpen"
:panel-mode="panelMode"
@toggle-panel-menu="togglePanelMenu"
@select-panel="selectPanelMode"
@open-file-manager="openGuiFileManager"
/>
<div class="resize-handle" @mousedown="startResize('left', $event)"></div>
<main class="chat-container">
<TokenDrawer
:visible="Boolean(currentConversationId)"
:collapsed="tokenPanelCollapsed"
:current-conversation-tokens="currentConversationTokens"
:current-context-tokens="currentContextTokens"
:container-status="containerStatus"
:container-net-rate="containerNetRate"
:project-storage="projectStorage"
:usage-quota="usageQuota"
:format-token-count="formatTokenCount"
:format-percentage="formatPercentage"
:format-bytes="formatBytes"
:format-quota-value="formatQuotaValue"
:format-reset-time="formatResetTime"
:format-rate="formatRate"
/>
<ChatArea
ref="messagesArea"
:messages="messages"
:icon-style="iconStyle"
:expanded-blocks="expandedBlocks"
:render-markdown="renderMarkdown"
:toggle-block="toggleBlock"
:handle-thinking-scroll="handleThinkingScroll"
:streaming-message="streamingMessage"
:get-tool-icon="getToolIcon"
:get-tool-status-text="getToolStatusText"
:get-tool-animation-class="getToolAnimationClass"
:get-tool-description="getToolDescription"
:format-search-topic="formatSearchTopic"
:format-search-time="formatSearchTime"
/>
<div class="scroll-lock-toggle" :class="{ locked: autoScrollEnabled && !userScrolling }">
<button @click="toggleScrollLock" class="scroll-lock-btn">
<svg
v-if="autoScrollEnabled && !userScrolling"
viewBox="0 0 24 24"
aria-hidden="true"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M8 11h8a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1v-6a1 1 0 0 1 1-1Z" />
<path d="M9 11V8a3 3 0 0 1 6 0v3" />
</svg>
<svg v-else viewBox="0 0 24 24" aria-hidden="true" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v12" />
<path d="M7 13l5 5 5-5" />
</svg>
</button>
</div>
<InputComposer
ref="inputComposer"
:input-message="inputMessage"
:input-is-multiline="inputIsMultiline"
:input-is-focused="inputIsFocused"
:is-connected="isConnected"
:streaming-message="streamingMessage"
:uploading="uploading"
:thinking-mode="thinkingMode"
:quick-menu-open="quickMenuOpen"
:tool-menu-open="toolMenuOpen"
:tool-settings="toolSettings"
:tool-settings-loading="toolSettingsLoading"
:settings-open="settingsOpen"
:compressing="compressing"
:current-conversation-id="currentConversationId"
:icon-style="iconStyle"
:tool-category-icon="toolCategoryIcon"
@update:input-message="inputSetMessage"
@input-change="handleInputChange"
@input-focus="handleInputFocus"
@input-blur="handleInputBlur"
@toggle-quick-menu="toggleQuickMenu"
@send-message="sendMessage"
@send-or-stop="handleSendOrStop"
@quick-upload="handleQuickUpload"
@toggle-tool-menu="toggleToolMenu"
@quick-mode-toggle="handleQuickModeToggle"
@toggle-settings="toggleSettings"
@update-tool-category="updateToolCategory"
@realtime-terminal="handleRealtimeTerminalClick"
@toggle-focus-panel="handleFocusPanelToggleClick"
@toggle-token-panel="handleTokenPanelToggleClick"
@compress-conversation="handleCompressConversationClick"
@file-selected="handleFileSelected"
/>
</main>
<div class="resize-handle" @mousedown="startResize('right', $event)"></div>
<FocusPanel
:collapsed="rightCollapsed"
:width="rightWidth"
:icon-style="iconStyle"
:get-language-class="getLanguageClass"
/>
</div>
<PersonalizationDrawer />
</template>
</AppShell>
</template>
<script setup lang="ts">

View File

@ -2,7 +2,6 @@
// static/app-enhanced.js - 修复版正确实现Token实时更新
import katex from 'katex';
import { mapActions, mapState, mapWritableState } from 'pinia';
import FileNode from './components/files/FileNode.vue';
import ChatArea from './components/chat/ChatArea.vue';
import ConversationSidebar from './components/sidebar/ConversationSidebar.vue';
import LeftPanel from './components/panels/LeftPanel.vue';
@ -2156,7 +2155,6 @@ const appOptions = {
};
(appOptions as any).components = {
FileNode,
ChatArea,
ConversationSidebar,
LeftPanel,

View File

@ -11,7 +11,18 @@
>
<span class="sr-only">展开对话记录</span>
<span class="chat-icon" aria-hidden="true">
<slot name="collapsed-chat-icon" />
<slot name="collapsed-chat-icon">
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M5 6.5c0-1.38 1.12-2.5 2.5-2.5h13c1.38 0 2.5 1.12 2.5 2.5v8.5c0 1.38-1.12 2.5-2.5 2.5h-5.6l-3.4 3.2.6-3.2H7.5c-1.38 0-2.5-1.12-2.5-2.5V6.5z"
stroke="currentColor"
stroke-width="1.7"
stroke-linejoin="round"
/>
<path d="M9 9.5h10" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
<path d="M9 13h6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" />
</svg>
</slot>
</span>
</button>
<button
@ -22,7 +33,25 @@
>
<span class="sr-only">新建对话</span>
<span class="pencil-icon" aria-hidden="true">
<slot name="collapsed-create-icon" />
<slot name="collapsed-create-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.8"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"
fill="none"
/>
<path d="m15 5 4 4" fill="none" />
</svg>
</slot>
</span>
</button>
</div>

View File

@ -135,8 +135,11 @@
}
.pencil-icon path {
fill: currentColor;
fill-opacity: 0.92;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.new-conversation-btn {