feat: 优化配置侧边栏与输入区交互,增强用户体验

This commit is contained in:
Wenjie Zhang 2026-04-18 16:22:59 +08:00
parent dda3009611
commit 4035f3c02d
5 changed files with 369 additions and 189 deletions

View File

@ -32,7 +32,7 @@
<!-- 0.6.1 的内容请放在这里 -->
- 合并知识库导航入口:左侧导航仅保留“知识库”,文档知识库与图知识库在页面 header 中通过同一组轻量切换入口切换,保留原有列表与图谱内容区交互。
- 抽象页面轻量切换 header知识库与扩展管理页直接共用 `ViewSwitchHeader`,通过统一切换样式和 actions slot 收敛文档知识库、知识图谱、Tools、MCP、Subagents、Skills 等入口的信息层级;扩展管理各列表的刷新入口下沉到搜索框右侧,并统一搜索框与工具按钮的边框和圆角。
- 抽象页面轻量切换 header知识库与扩展管理页直接共用 `ViewSwitchHeader`,通过统一切换样式和 actions slot 收敛文档知识库、知识图谱、Tools、MCP、Subagents、Skills 等入口的信息层级;扩展管理各列表的刷新入口下沉到搜索框右侧,并统一搜索框与工具按钮的边框和圆角,同时强化切换项选中态的边框、阴影与字重层级,提升 header 中当前视图的辨识度
- 调整任务中心交互:入口移动到 GitHub 按钮下方,并将右侧抽屉展示改为居中弹窗,减少对主页面布局的占用。
- 调整 backend Python 工作区依赖边界:将 `backend/package/yuxi` 明确为承载核心运行依赖的业务包,根 `backend/pyproject.toml` 仅保留工作区入口与开发/测试配置,减少依赖职责混淆。
- 将 `yuxi` 从 uv workspace 成员调整为 `backend/package` 下可独立构建的本地 Python 包backend 通过 path dependency 以已安装包形式发现依赖,移除对 `PYTHONPATH=/app/package` 的运行时耦合。
@ -53,6 +53,7 @@
- 修复对话摘要中间件的工具结果卸载链路:摘要触发时改为将大体积 `ToolMessage` 写入当前 agent 可见的 sandbox outputs 路径,修正 `summary_offload` 路径拼接错误、`messages` 触发条件下不会真正裁剪历史的问题,并避免将 system message 重复纳入摘要与最终消息列表;补充对应单元测试覆盖。
- 调整智能体对话中的工具调用展示:连续工具调用默认折叠为“调用了 N 个工具”的轻量摘要,展开后改为弱化时间线样式,减少工具结果卡片对正文阅读节奏的干扰。
- 调整聊天首页的智能体切换入口:在无历史对话时,智能体数量 `<= 3``chat-main` 宽度不小于 `380px` 时继续使用横向 segmented当智能体数量 `>= 4` 或内容区宽度小于 `380px` 时自动收敛为“当前智能体 + 下拉按钮”形式。
- 调整对话配置入口与侧边栏头尾交互:输入区配置按钮改为轻量 dropdown 触发器默认保持透明、hover 才出现背景;下拉中直接提供配置切换、新建配置,以及查看/编辑当前配置的入口。配置侧边栏则移除头部配置切换和新建入口,改为仅显示当前配置名称、可点击星标和关闭图标,并将删除操作下沉到底部保存按钮右侧。
---

View File

@ -3,47 +3,27 @@
<!-- 侧边栏头部 -->
<div class="sidebar-header">
<div class="header-top-row">
<div v-if="selectedAgentId" class="config-manage-row">
<a-select
:value="selectedAgentConfigId"
:options="configSwitchOptions"
class="config-switch-select"
placeholder="选择配置"
@update:value="handleConfigSwitch"
/>
<div v-if="selectedAgentId" class="config-title-row">
<span class="config-title-text">{{ currentConfigName }}</span>
</div>
<div class="header-actions">
<a-tooltip
v-if="!isEmptyConfig && userStore.isAdmin"
:title="isCurrentDefault ? '当前已是默认配置' : '设为默认配置'"
>
<a-button
type="text"
shape="circle"
class="icon-btn lucide-icon-btn"
<button
type="button"
class="header-icon-action"
:class="{ 'is-default': isCurrentDefault }"
@click="setAsDefault"
>
<Star :size="18" :fill="isCurrentDefault ? 'currentColor' : 'none'" />
</a-button>
</button>
</a-tooltip>
<a-tooltip v-if="!isEmptyConfig && userStore.isAdmin" title="删除配置">
<a-button
type="text"
shape="circle"
danger
class="icon-btn lucide-icon-btn"
@click="confirmDeleteConfig"
:disabled="isDeletingConfig"
>
<Trash2 :size="18" />
</a-button>
</a-tooltip>
<a-button type="text" size="small" @click="closeSidebar" class="icon-btn lucide-icon-btn">
<button type="button" @click="closeSidebar" class="header-icon-action">
<X :size="16" />
</a-button>
</button>
</div>
</div>
</div>
@ -78,7 +58,6 @@
<!-- 统一显示所有配置项 -->
<template v-for="(value, key) in filteredConfigurableItems" :key="key">
<a-form-item
v-if="shouldShowConfig(key, value)"
:label="getConfigLabel(key, value)"
:name="key"
class="config-item"
@ -312,22 +291,20 @@
>
保存
</a-button>
<a-tooltip v-if="!isEmptyConfig" title="删除配置">
<button
type="button"
class="footer-icon-btn"
@click="confirmDeleteConfig"
:disabled="isDeletingConfig"
>
<Trash2 :size="16" />
</button>
</a-tooltip>
</div>
</div>
<!-- 通用选择弹窗 -->
<a-modal
v-model:open="createConfigModalOpen"
title="新建配置"
:width="360"
:confirmLoading="createConfigLoading"
@ok="handleCreateConfig"
@cancel="closeCreateConfigModal"
>
<a-input v-model:value="createConfigName" placeholder="请输入配置名称" allow-clear />
</a-modal>
<a-modal
v-model:open="selectionModalOpen"
:title="`选择${configurableItems[currentConfigKey]?.name || '项目'}`"
@ -526,10 +503,6 @@ const liveSkillOptions = ref([])
const liveMcpOptions = ref([])
const liveSubagentOptions = ref([])
const toolOptionsFromApi = ref([])
const createConfigModalOpen = ref(false)
const createConfigLoading = ref(false)
const createConfigName = ref('')
const CREATE_CONFIG_OPTION_VALUE = '__create_config__'
const currentSegment = ref('model')
const segmentOptions = [
{ label: '模型', value: 'model' },
@ -545,6 +518,10 @@ const isCurrentDefault = computed(() => {
return !!selectedConfigSummary.value?.is_default
})
const currentConfigName = computed(() => {
return selectedConfigSummary.value?.name || '当前配置'
})
const isReadOnlyConfig = computed(() => !userStore.isAdmin)
const isSavingConfig = ref(false)
@ -578,24 +555,6 @@ const filteredConfigurableItems = computed(() => {
return filtered
})
const configSwitchOptions = computed(() => {
if (!selectedAgentId.value) return []
const list = agentConfigs.value[selectedAgentId.value] || []
const options = list.map((cfg) => ({
label: cfg.is_default ? `${cfg.name}(默认)` : cfg.name,
value: cfg.id
}))
if (userStore.isAdmin) {
options.push({
label: '+ 新建配置',
value: CREATE_CONFIG_OPTION_VALUE
})
}
return options
})
const loadLiveSkillOptions = async (force = false) => {
if (!userStore.isAdmin) {
liveSkillOptions.value = []
@ -834,21 +793,6 @@ const filteredOptions = computed(() => {
})
//
const handleConfigSwitch = async (configId) => {
if (configId === CREATE_CONFIG_OPTION_VALUE) {
openCreateConfigModal()
return
}
if (!configId || configId === selectedAgentConfigId.value) return
try {
await agentStore.selectAgentConfig(configId)
} catch (error) {
console.error('切换配置出错:', error)
message.error('切换配置失败')
}
}
const updateConfigValue = (key, value) => {
if (isReadOnlyConfig.value) return
agentStore.updateAgentConfig({
@ -856,47 +800,6 @@ const updateConfigValue = (key, value) => {
})
}
const openCreateConfigModal = () => {
if (!userStore.isAdmin) return
createConfigName.value = ''
createConfigModalOpen.value = true
}
const closeCreateConfigModal = () => {
createConfigModalOpen.value = false
createConfigName.value = ''
}
const handleCreateConfig = async () => {
if (!userStore.isAdmin) return
if (!selectedAgentId.value) return
const name = createConfigName.value.trim()
if (!name) {
message.error('请输入配置名称')
return
}
createConfigLoading.value = true
try {
await agentStore.createAgentConfigProfile({
name,
setDefault: false,
fromCurrent: false
})
closeCreateConfigModal()
message.success('配置已创建')
} catch (error) {
console.error('创建配置出错:', error)
message.error(error.message || '创建配置失败')
} finally {
createConfigLoading.value = false
}
}
const shouldShowConfig = () => {
return true
}
const closeSidebar = () => {
emit('close')
}
@ -1185,81 +1088,61 @@ const confirmDeleteConfig = async () => {
width: 100%;
}
.config-manage-row {
.config-title-row {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.config-switch-select {
flex: 1;
min-width: 0;
:deep(.ant-select-selector) {
height: 32px;
border-radius: 8px;
border-color: var(--gray-200);
padding: 0 10px;
transition: border-color 0.2s ease;
}
:deep(.ant-select-selection-search-input),
:deep(.ant-select-selection-item),
:deep(.ant-select-selection-placeholder) {
line-height: 30px;
font-size: 13px;
}
:deep(.ant-select.ant-select-focused .ant-select-selector),
:deep(.ant-select-selector:hover) {
border-color: var(--main-color);
box-shadow: none;
}
}
.config-title-text {
min-width: 0;
font-size: 14px;
font-weight: 600;
color: var(--gray-900);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
gap: 4px;
margin-left: auto;
}
}
.icon-btn {
width: 32px;
height: 32px;
border-radius: 8px;
color: var(--gray-600);
border: 1px solid var(--gray-200);
background: var(--gray-0);
.header-icon-action {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: none;
background: transparent;
border-radius: 6px;
color: var(--gray-600);
transition:
color 0.2s ease,
border-color 0.2s ease,
background-color 0.2s ease;
cursor: pointer;
&:hover:not(:disabled) {
color: var(--main-600);
border-color: var(--main-200);
background: var(--main-10);
background: var(--gray-100);
}
&.is-default {
&.is-default,
&.is-default:hover:not(:disabled) {
color: var(--color-warning-500);
}
&.ant-btn-dangerous:hover:not(:disabled) {
color: var(--color-error-700);
border-color: var(--color-error-100);
background: var(--color-error-50);
}
&:disabled {
cursor: not-allowed;
background: transparent;
color: var(--gray-400);
border-color: var(--gray-200);
&.is-default {
opacity: 1;
@ -1450,7 +1333,7 @@ const confirmDeleteConfig = async () => {
align-items: center;
.footer-main-btn {
width: 100%;
flex: 1;
height: 36px;
border-radius: 8px;
font-size: 14px;
@ -1484,6 +1367,36 @@ const confirmDeleteConfig = async () => {
color: var(--gray-400);
}
}
.footer-icon-btn {
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid var(--gray-200);
border-radius: 8px;
background: var(--gray-0);
color: var(--gray-500);
cursor: pointer;
transition:
color 0.2s ease,
border-color 0.2s ease,
background-color 0.2s ease;
&:hover:not(:disabled) {
color: var(--color-error-700);
border-color: var(--color-error-100);
background: var(--color-error-50);
}
&:disabled {
cursor: not-allowed;
color: var(--gray-400);
background: var(--gray-50);
}
}
}
}
}

View File

@ -252,7 +252,7 @@ const getTodoStatusLabel = (status) => {
padding: 6px 8px;
// height: 28px;
border-radius: 8px;
font-size: 14px;
font-size: 13px;
color: var(--gray-600);
cursor: pointer;
transition: all 0.2s ease;

View File

@ -83,10 +83,10 @@ const selectItem = (item) => {
align-items: center;
height: 28px;
padding: 0 10px;
border: 0;
border: 1px solid transparent;
border-radius: 6px;
background: transparent;
color: var(--gray-700);
color: var(--gray-600);
font-size: 15px;
font-weight: 500;
line-height: 1;
@ -94,16 +94,26 @@ const selectItem = (item) => {
cursor: pointer;
transition:
background-color 0.2s ease,
color 0.2s ease;
color 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
&:hover {
color: var(--main-color);
color: var(--gray-900);
background-color: var(--gray-50);
}
&:focus-visible {
outline: 2px solid var(--main-200);
outline-offset: 2px;
}
&.active {
color: var(--main-color);
background-color: var(--main-40);
color: var(--main-700);
font-weight: 600;
background-color: var(--main-50);
border-color: var(--main-200);
box-shadow: 0 1px 2px rgba(1, 21, 31, 0.06);
}
}
</style>

View File

@ -9,18 +9,67 @@
@thread-change="handleThreadChange"
>
<template #input-actions-left>
<button
<a-dropdown
v-if="selectedAgentId"
class="input-action-btn"
:class="{ active: chatUIStore.isConfigSidebarOpen }"
:disabled="isLoadingConfig"
@click="openConfigSidebar"
v-model:open="configDropdownOpen"
:trigger="['click']"
placement="topLeft"
overlay-class-name="config-dropdown-overlay"
>
<Settings2 size="18" />
<span class="hide-text">
{{ isLoadingConfig ? '加载中...' : selectedConfigSummary?.name || '配置' }}
</span>
</button>
<button
type="button"
class="input-action-btn config-dropdown-trigger"
:class="{ disabled: isLoadingConfig }"
@click.stop
@mousedown.stop
>
<Settings2 size="18" class="nav-btn-icon" />
<span class="hide-text config-dropdown-text">{{ currentConfigLabel }}</span>
<ChevronDown size="15" class="config-dropdown-chevron" />
</button>
<template #overlay>
<div class="config-dropdown-panel" @click.stop>
<button
v-for="config in configQuickSwitchOptions"
:key="config.value"
type="button"
class="config-dropdown-item"
:class="{ selected: config.value === selectedAgentConfigId }"
@click="handleConfigSwitch(config.value)"
>
<span class="config-dropdown-item-label">{{ config.label }}</span>
<span v-if="config.isDefault" class="config-dropdown-item-badge">默认</span>
<Check
v-if="config.value === selectedAgentConfigId"
:size="14"
class="config-dropdown-item-check"
/>
</button>
<div class="config-dropdown-divider"></div>
<button
type="button"
class="config-dropdown-item action-item"
@click="toggleConfigSidebar"
>
<Settings2 :size="15" class="config-dropdown-item-icon" />
<span class="config-dropdown-item-label">{{ configSidebarActionLabel }}</span>
</button>
<button
v-if="userStore.isAdmin"
type="button"
class="config-dropdown-item action-item"
@click="openCreateConfigModal"
>
<Plus :size="15" class="config-dropdown-item-icon" />
<span class="config-dropdown-item-label">新建配置</span>
</button>
</div>
</template>
</a-dropdown>
</template>
<template #header-right v-if="userStore.isAdmin">
@ -50,6 +99,17 @@
:agent-id="selectedAgentId"
/>
<a-modal
v-model:open="createConfigModalOpen"
title="新建配置"
:width="360"
:confirm-loading="createConfigLoading"
@ok="handleCreateConfig"
@cancel="closeCreateConfigModal"
>
<a-input v-model:value="createConfigName" placeholder="请输入配置名称" allow-clear />
</a-modal>
<!-- 自定义更多菜单 -->
<Teleport to="body">
<Transition name="menu-fade">
@ -78,10 +138,10 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { MessageOutlined, ShareAltOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { Settings2, Ellipsis } from 'lucide-vue-next'
import { Settings2, Ellipsis, ChevronDown, Check, Plus } from 'lucide-vue-next'
import { useRoute, useRouter } from 'vue-router'
import AgentChatComponent from '@/components/AgentChatComponent.vue'
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue'
@ -107,8 +167,13 @@ const route = useRoute()
const router = useRouter()
// agentStore
const { selectedAgentId, defaultAgentId, selectedConfigSummary, isLoadingConfig } =
storeToRefs(agentStore)
const {
selectedAgentId,
defaultAgentId,
selectedAgentConfigId,
agentConfigs,
isLoadingConfig
} = storeToRefs(agentStore)
const syncingRouteThread = ref(false)
@ -171,8 +236,87 @@ const handleThreadChange = (threadId) => {
}
}
const openConfigSidebar = () => {
// /
const configQuickSwitchOptions = computed(() => {
if (!selectedAgentId.value) return []
const list = agentConfigs.value[selectedAgentId.value] || []
return list.map((config) => ({
label: config.name,
value: config.id,
isDefault: !!config.is_default
}))
})
const currentConfigLabel = computed(() => {
if (isLoadingConfig.value) return '加载中...'
const current = configQuickSwitchOptions.value.find(
(config) => config.value === selectedAgentConfigId.value
)
return current?.label || '配置'
})
const configSidebarActionLabel = computed(() => {
return chatUIStore.isConfigSidebarOpen ? '收起配置侧边栏' : '查看/编辑配置'
})
const configDropdownOpen = ref(false)
const createConfigModalOpen = ref(false)
const createConfigLoading = ref(false)
const createConfigName = ref('')
const handleConfigSwitch = async (configId) => {
if (!configId || configId === selectedAgentConfigId.value) return
try {
await agentStore.selectAgentConfig(configId)
configDropdownOpen.value = false
} catch (error) {
console.error('切换配置出错:', error)
message.error('切换配置失败')
}
}
const toggleConfigSidebar = () => {
chatUIStore.isConfigSidebarOpen = !chatUIStore.isConfigSidebarOpen
configDropdownOpen.value = false
}
const openCreateConfigModal = () => {
if (!userStore.isAdmin) return
configDropdownOpen.value = false
createConfigName.value = ''
createConfigModalOpen.value = true
}
const closeCreateConfigModal = () => {
createConfigModalOpen.value = false
createConfigName.value = ''
}
const handleCreateConfig = async () => {
if (!userStore.isAdmin || !selectedAgentId.value) return
const name = createConfigName.value.trim()
if (!name) {
message.error('请输入配置名称')
return
}
createConfigLoading.value = true
try {
await agentStore.createAgentConfigProfile({
name,
setDefault: false,
fromCurrent: false
})
closeCreateConfigModal()
chatUIStore.isConfigSidebarOpen = true
message.success('配置已创建')
} catch (error) {
console.error('创建配置出错:', error)
message.error(error.message || '创建配置失败')
} finally {
createConfigLoading.value = false
}
}
//
@ -282,6 +426,36 @@ const handleFeedback = () => {
overflow: hidden;
}
.config-dropdown-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 0;
max-width: min(240px, calc(100vw - 160px));
gap: 4px;
}
.config-dropdown-trigger .nav-btn-icon {
color: currentColor;
}
.config-dropdown-trigger :deep(svg) {
color: currentColor;
}
.config-dropdown-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: currentColor;
}
.config-dropdown-chevron {
flex-shrink: 0;
color: currentColor;
}
//
.more-popup-menu {
position: fixed;
@ -374,6 +548,10 @@ const handleFeedback = () => {
//
@media (max-width: 520px) {
.config-dropdown-trigger {
max-width: calc(100vw - 112px);
}
.more-popup-menu {
box-shadow:
0 12px 32px rgba(0, 0, 0, 0.12),
@ -381,3 +559,81 @@ const handleFeedback = () => {
}
}
</style>
<style lang="less">
.config-dropdown-overlay .config-dropdown-panel {
min-width: 188px;
max-width: min(260px, calc(100vw - 24px));
padding: 4px;
background: var(--gray-0);
border: 1px solid var(--gray-100);
border-radius: 8px;
box-shadow:
0 8px 24px rgba(0, 0, 0, 0.08),
0 2px 8px rgba(0, 0, 0, 0.04);
}
.config-dropdown-overlay .config-dropdown-item {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
width: 100%;
padding: 6px 8px;
border: none;
border-radius: 6px;
background: transparent;
text-align: left;
cursor: pointer;
transition: background-color 0.15s ease;
}
.config-dropdown-overlay .config-dropdown-item:hover {
background: var(--gray-50);
}
.config-dropdown-overlay .config-dropdown-item.selected {
background: var(--gray-50);
}
.config-dropdown-overlay .config-dropdown-item.action-item {
color: var(--gray-800);
}
.config-dropdown-overlay .config-dropdown-item-label {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
line-height: 1.35;
color: var(--gray-800);
}
.config-dropdown-overlay .config-dropdown-item-icon {
flex-shrink: 0;
color: var(--gray-500);
}
.config-dropdown-overlay .config-dropdown-item-badge {
flex-shrink: 0;
padding: 1px 6px;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-600);
font-size: 11px;
line-height: 1.4;
}
.config-dropdown-overlay .config-dropdown-item-check {
flex-shrink: 0;
color: var(--main-600);
}
.config-dropdown-overlay .config-dropdown-divider {
height: 1px;
margin: 4px 4px;
background: var(--gray-100);
}
</style>