feat(ui): 重构智能体配置为侧边栏形式并优化交互

将智能体配置从弹窗改为侧边栏形式,提升用户体验
添加配置侧边栏与聊天侧边栏的互斥逻辑
在智能体选择按钮上添加图标增强视觉提示
移除已完成的任务项标记
This commit is contained in:
Wenjie Zhang 2025-08-22 23:17:48 +08:00
parent fe6c9ea364
commit 9b96bd0ba6
4 changed files with 1107 additions and 324 deletions

View File

@ -20,7 +20,6 @@
- [x] 文件上传模块 UI的边距、配色有问题 - [x] 文件上传模块 UI的边距、配色有问题
- [x] 知识图谱页面的节点数量统计方法 #236 - [x] 知识图谱页面的节点数量统计方法 #236
- [x] 当没有手动添加节点的时候,尝试检索,会出现为创建索引的情况 #236 - [x] 当没有手动添加节点的时候,尝试检索,会出现为创建索引的情况 #236
- [ ] 删除知识库的时候,没有正常将数据从 Neo4j / Milvus 数据库中删除(需要添加检测脚本)
# 💯 More: # 💯 More:

View File

@ -1222,7 +1222,6 @@ watch([convs, () => onGoingConv.messages], () => {
scrollToBottom(); scrollToBottom();
}, { deep: true }); }, { deep: true });
// ==================== ==================== // ==================== ====================
// //
@ -1289,6 +1288,10 @@ const scrollToBottom = async () => {
const toggleSidebar = () => { const toggleSidebar = () => {
state.isSidebarOpen = !state.isSidebarOpen; state.isSidebarOpen = !state.isSidebarOpen;
localStorage.setItem('chat_sidebar_open', state.isSidebarOpen); localStorage.setItem('chat_sidebar_open', state.isSidebarOpen);
//
if (state.isSidebarOpen) {
emit('close-config-sidebar');
}
console.log("toggleSidebar", state.isSidebarOpen); console.log("toggleSidebar", state.isSidebarOpen);
} }

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
<div class="header-left"> <div class="header-left">
<div class="header-item"> <div class="header-item">
<a-button class="header-button" @click="openAgentModal"> <a-button class="header-button" @click="openAgentModal">
<Bot size="18" stroke-width="1.75" />
选择{{ selectedAgent.name || '选择智能体' }} 选择{{ selectedAgent.name || '选择智能体' }}
</a-button> </a-button>
</div> </div>
@ -27,213 +28,6 @@
</div> </div>
</div> </div>
<div class="agent-view-body"> <div class="agent-view-body">
<!-- 配置弹窗 -->
<a-modal
v-model:open="state.agentConfOpen"
title="智能体详细配置"
:width="800"
:footer="null"
:maskClosable="false"
class="conf-modal"
>
<div class="conf-content">
<div class="agent-info">
<p>详细配置信<span @click="toggleDebugMode"></span></p>
<p>{{ selectedAgent.description }}</p>
<pre v-if="state.debug_mode">{{ selectedAgent }}</pre>
<a-divider />
<div v-if="selectedAgentId && configSchema" class="config-modal-content">
<!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical">
<a-alert v-if="state.isEmptyConfig" type="warning" message="该智能体没有配置项" show-icon/>
<a-alert v-if="!selectedAgent.has_checkpointer" type="error" message="该智能体没有配置 Checkpointer功能无法正常使用参考https://langchain-ai.github.io/langgraph/concepts/persistence/" show-icon/>
<!-- 统一显示所有配置项 -->
<template v-for="(value, key) in configurableItems" :key="key">
<a-form-item
:label="getConfigLabel(key, value)"
:name="key"
class="config-item"
>
<p v-if="value.description" class="description">{{ value.description }}</p>
<!-- key匹配 -->
<div v-if="key === 'model'" class="agent-model">
<!-- <p><small>注意部分模型对于 Tool Calling 的支持不稳定建议采用{{ value.options }} </small></p> -->
<ModelSelectorComponent
@select-model="handleModelChange"
:model_name="agentConfig[key] ? agentConfig[key].split('/').slice(1).join('/') : ''"
:model_provider="agentConfig[key] ? agentConfig[key].split('/')[0] : ''"
/>
</div>
<a-textarea
v-else-if="key === 'system_prompt'"
v-model:value="agentConfig[key]"
:rows="2"
:placeholder="getPlaceholder(key, value)"
/>
<!-- 工具选择特殊处理 -->
<div v-else-if="key === 'tools'" class="tools-selector">
<div class="tools-summary">
<div class="tools-summary-left">
<span class="tools-count">已选择 {{ getSelectedCount(key) }} 个工具</span>
<a-button type="link" size="small" @click="clearSelection(key)" v-if="getSelectedCount(key) > 0">
清空
</a-button>
</div>
<a-button type="primary" @click="openToolsModal" class="select-tools-btn">
选择工具
</a-button>
</div>
<div v-if="getSelectedCount(key) > 0" class="selected-tools-preview">
<a-tag v-for="toolId in agentConfig[key]" :key="toolId" closable @close="removeSelectedTool(toolId)">
{{ getToolNameById(toolId) }}
</a-tag>
</div>
</div>
<!-- 数据类型匹配 -->
<!-- 布尔类型 -->
<a-switch
v-else-if="typeof agentConfig[key] === 'boolean'"
v-model:checked="agentConfig[key]"
/>
<!-- 单选 -->
<a-select
v-else-if="value?.options && (value?.type === 'str' || value?.type === 'select')"
v-model:value="agentConfig[key]"
>
<a-select-option v-for="option in value.options" :key="option" :value="option">
{{ option.label || option }}
</a-select-option>
</a-select>
<!-- 多选 -->
<div v-else-if="value?.options && value?.type === 'list'" class="multi-select-cards">
<div class="multi-select-label">
<span>已选择 {{ getSelectedCount(key) }} </span>
<a-button type="link" size="small" @click="clearSelection(key)" v-if="getSelectedCount(key) > 0" >
清空
</a-button>
</div>
<div class="options-grid">
<div
v-for="option in value.options"
:key="option"
class="option-card"
:class="{
'selected': isOptionSelected(key, option),
'unselected': !isOptionSelected(key, option)
}"
@click="toggleOption(key, option)"
>
<div class="option-content">
<span class="option-text">{{ getToolNameById(toolId) }}</span>
<div class="option-indicator">
<CheckCircleOutlined v-if="isOptionSelected(key, option)" />
<PlusCircleOutlined v-else />
</div>
</div>
</div>
</div>
</div>
<!-- 数字 -->
<a-input-number
v-else-if="value?.type === 'number'"
v-model:value="agentConfig[key]"
:placeholder="getPlaceholder(key, value)"
/>
<!-- 滑块 -->
<a-slider
v-else-if="value?.type === 'slider'"
v-model:value="agentConfig[key]"
:min="value.min"
:max="value.max"
:step="value.step"
style="max-width: 300px;"
/>
<!-- 其他类型 -->
<a-input
v-else
v-model:value="agentConfig[key]"
:placeholder="getPlaceholder(key, value)"
/>
</a-form-item>
</template>
<a-divider />
<!-- 弹窗底部按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig">
<div class="form-actions-left">
<a-button type="primary" @click="saveConfig">保存并发布配置</a-button>
<a-button @click="resetConfig">重置</a-button>
</div>
</div>
</a-form>
</div>
</div>
</div>
</a-modal>
<!-- 工具选择弹窗 -->
<a-modal
v-model:open="state.toolsModalOpen"
title="选择工具"
:width="800"
:footer="null"
:maskClosable="false"
class="tools-modal"
>
<div class="tools-modal-content">
<div class="tools-search">
<a-input
v-model:value="toolsSearchText"
placeholder="搜索工具..."
allow-clear
/>
</div>
<div class="tools-list">
<div
v-for="tool in filteredTools"
:key="tool.id"
class="tool-item"
:class="{ 'selected': selectedTools.includes(tool.id) }"
@click="toggleToolSelection(tool.id)"
>
<div class="tool-content">
<div class="tool-header">
<span class="tool-name">{{ tool.name }}</span>
<div class="tool-indicator">
<CheckCircleOutlined v-if="selectedTools.includes(tool.id)" />
<PlusCircleOutlined v-else />
</div>
</div>
<div class="tool-description">{{ tool.description }}</div>
</div>
</div>
</div>
<div class="tools-modal-footer">
<div class="selected-count">
已选择 {{ selectedTools.length }} 个工具
</div>
<div class="modal-actions">
<a-button @click="cancelToolsSelection">取消</a-button>
<a-button type="primary" @click="confirmToolsSelection">确认</a-button>
</div>
</div>
</div>
</a-modal>
<!-- 智能体选择弹窗 --> <!-- 智能体选择弹窗 -->
<a-modal <a-modal
v-model:open="state.agentModalOpen" v-model:open="state.agentModalOpen"
@ -274,9 +68,22 @@
:single-mode="false" :single-mode="false"
@open-config="toggleConf" @open-config="toggleConf"
@open-agent-modal="openAgentModal" @open-agent-modal="openAgentModal"
@close-config-sidebar="() => state.isConfigSidebarOpen = false"
> >
</AgentChatComponent> </AgentChatComponent>
</div> </div>
<!-- 配置侧边栏 -->
<AgentConfigSidebar
:isOpen="state.isConfigSidebarOpen"
:selectedAgent="selectedAgent"
:selectedAgentId="selectedAgentId"
:agentConfig="agentConfig"
@close="() => state.isConfigSidebarOpen = false"
@update:agentConfig="(config) => agentConfig = config"
@config-saved="loadAgentConfig"
@config-reset="loadAgentConfig"
/>
</div> </div>
</div> </div>
</template> </template>
@ -294,12 +101,13 @@ import {
PlusCircleOutlined PlusCircleOutlined
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { Bot } from 'lucide-vue-next';
import AgentChatComponent from '@/components/AgentChatComponent.vue'; import AgentChatComponent from '@/components/AgentChatComponent.vue';
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'; import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue';
import AgentConfigSidebar from '@/components/AgentConfigSidebar.vue';
import { useUserStore } from '@/stores/user'; import { useUserStore } from '@/stores/user';
import { chatApi } from '@/apis/auth_api'; import { chatApi } from '@/apis/auth_api';
import { agentConfigApi } from '@/apis/system_api'; import { agentConfigApi } from '@/apis/system_api';
import { toolsApi } from '@/apis/tools';
// //
const router = useRouter(); const router = useRouter();
@ -310,38 +118,16 @@ const agents = ref({});
const selectedAgentId = ref(null); const selectedAgentId = ref(null);
const defaultAgentId = ref(null); // ID const defaultAgentId = ref(null); // ID
const state = reactive({ const state = reactive({
agentConfOpen: false,
debug_mode: false, debug_mode: false,
toolsModalOpen: false,
agentModalOpen: false, agentModalOpen: false,
isConfigSidebarOpen: false,
isEmptyConfig: computed(() => isEmptyConfig: computed(() =>
!selectedAgentId.value || !selectedAgentId.value ||
Object.keys(configurableItems.value).length === 0 Object.keys(configurableItems.value).length === 0
) )
}); });
//
const availableTools = ref([]);
const selectedTools = ref([]);
const toolsSearchText = ref('');
//
const filteredTools = computed(() => {
if (!toolsSearchText.value) {
return availableTools.value;
}
const searchLower = toolsSearchText.value.toLowerCase();
return availableTools.value.filter(tool =>
tool.name.toLowerCase().includes(searchLower) ||
tool.description.toLowerCase().includes(searchLower)
);
});
// ID
const getToolNameById = (toolId) => {
const tool = availableTools.value.find(t => t.id === toolId);
return tool ? tool.name : toolId;
};
const selectedAgent = computed(() => agents.value[selectedAgentId.value] || {}); const selectedAgent = computed(() => agents.value[selectedAgentId.value] || {});
const configSchema = computed(() => selectedAgent.value.config_schema || {}); const configSchema = computed(() => selectedAgent.value.config_schema || {});
@ -380,88 +166,9 @@ const setAsDefaultAgent = async () => {
} }
}; };
//
const ensureArray = (key) => {
if (!agentConfig.value[key] || !Array.isArray(agentConfig.value[key])) {
agentConfig.value[key] = [];
}
};
const isOptionSelected = (key, option) => {
ensureArray(key);
return agentConfig.value[key].includes(option);
};
const getSelectedCount = (key) => {
ensureArray(key);
return agentConfig.value[key].length;
};
const toggleOption = (key, option) => {
ensureArray(key);
const currentOptions = [...agentConfig.value[key]];
const index = currentOptions.indexOf(option);
if (index > -1) {
currentOptions.splice(index, 1);
} else {
currentOptions.push(option);
}
agentConfig.value[key] = currentOptions;
};
const clearSelection = (key) => {
agentConfig.value[key] = [];
};
//
const openToolsModal = async () => {
try {
//
if (availableTools.value.length === 0) {
await loadAvailableTools();
}
//
selectedTools.value = [...(agentConfig.value.tools || [])];
//
state.toolsModalOpen = true;
} catch (error) {
console.error('打开工具选择弹窗失败:', error);
message.error('打开工具选择弹窗失败');
}
};
const toggleToolSelection = (toolId) => {
const index = selectedTools.value.indexOf(toolId);
if (index > -1) {
selectedTools.value.splice(index, 1);
} else {
selectedTools.value.push(toolId);
}
};
const removeSelectedTool = (toolId) => {
const index = agentConfig.value.tools.indexOf(toolId);
if (index > -1) {
agentConfig.value.tools.splice(index, 1);
}
};
const confirmToolsSelection = () => {
agentConfig.value.tools = [...selectedTools.value];
state.toolsModalOpen = false;
toolsSearchText.value = '';
};
const cancelToolsSelection = () => {
state.toolsModalOpen = false;
toolsSearchText.value = '';
selectedTools.value = [];
};
// ID // ID
const fetchDefaultAgent = async () => { const fetchDefaultAgent = async () => {
@ -627,15 +334,7 @@ const selectAgentFromModal = (agentId) => {
state.agentModalOpen = false; state.agentModalOpen = false;
}; };
//
const loadAvailableTools = async () => {
try {
const response = await toolsApi.getTools();
availableTools.value = Object.values(response.tools || {});
} catch (error) {
console.error('加载工具列表失败:', error);
}
};
// //
onMounted(async () => { onMounted(async () => {
@ -644,7 +343,7 @@ onMounted(async () => {
// //
await fetchAgents(); await fetchAgents();
// //
await loadAvailableTools();
// //
const lastSelectedAgent = localStorage.getItem('last-selected-agent'); const lastSelectedAgent = localStorage.getItem('last-selected-agent');
@ -690,7 +389,11 @@ const toggleDebugMode = () => {
}; };
const toggleConf = () => { const toggleConf = () => {
state.agentConfOpen = !state.agentConfOpen state.isConfigSidebarOpen = !state.isConfigSidebarOpen
//
if (state.isConfigSidebarOpen) {
state.isChatSidebarOpen = false
}
} }
</script> </script>
@ -726,8 +429,11 @@ const toggleConf = () => {
align-items: center; align-items: center;
gap: 10px; gap: 10px;
button { button.header-button {
border-radius: 6px; border-radius: 6px;
display: flex;
align-items: center;
gap: 6px;
} }
} }
} }
@ -735,10 +441,12 @@ const toggleConf = () => {
.agent-view-body { .agent-view-body {
--gap-radius: 6px; --gap-radius: 6px;
display: flex; display: flex;
flex-direction: row;
width: 100%; width: 100%;
flex: 1; flex: 1;
min-height: calc(100% - var(--agent-view-header-height)); min-height: calc(100% - var(--agent-view-header-height));
overflow: hidden; overflow: hidden;
position: relative;
// padding: var(--gap-radius); // padding: var(--gap-radius);
// gap: var(--gap-radius); // gap: var(--gap-radius);