ForcePilot/web/src/views/AgentView.vue
2025-04-02 13:00:25 +08:00

554 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="agent-view">
<!-- 左侧智能体列表侧边栏 -->
<div class="sidebar" :class="{ 'is-open': state.isSidebarOpen }">
<h2 class="sidebar-title">
智能体列表
<div class="toggle-sidebar" @click="toggleSidebar">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
</div>
</h2>
<div class="agent-info">
<a-select
v-model:value="selectedAgentId"
class="agent-list"
style="width: 100%"
@change="selectAgent"
>
<a-select-option
v-for="(agent, name) in agents"
:key="name"
:value="name"
>
{{ agent.name }}
</a-select-option>
</a-select>
<p style="padding: 0 4px;">
{{ agents[selectedAgentId]?.description }}
</p>
<!-- 添加requirements显示部分 -->
<div v-if="agents[selectedAgentId]?.requirements && agents[selectedAgentId]?.requirements.length > 0" class="info-section">
<h3>所需环境变量:</h3>
<div class="requirements-list">
<a-tag v-for="req in agents[selectedAgentId].requirements" :key="req">
{{ req }}
</a-tag>
</div>
</div>
<!-- 添加all_tools显示部分 -->
<div v-if="agents[selectedAgentId]?.all_tools && agents[selectedAgentId]?.all_tools.length > 0" class="info-section">
<h3>可用工具:</h3>
<div class="all-tools-list">
<a-tag v-for="tool in agents[selectedAgentId].all_tools" :key="tool">
{{ tool }}
</a-tag>
</div>
</div>
</div>
</div>
<!-- 中间内容区域 -->
<div class="content">
<AgentChatComponent
:agent-id="selectedAgentId"
:config="agentConfig"
:state="state"
@open-config="toggleConfigSidebar(true)"
>
<template #header-left>
<div class="toggle-sidebar nav-btn" @click="toggleSidebar" v-if="!state.isSidebarOpen">
<img src="@/assets/icons/sidebar_left.svg" class="iconfont icon-20" alt="设置" />
</div>
</template>
<template #header-right>
<div class="toggle-sidebar nav-btn" @click="toggleConfigSidebar()">
<SettingOutlined class="iconfont icon-20" />
<span class="text">配置</span>
</div>
</template>
</AgentChatComponent>
</div>
<!-- 右侧配置侧边栏 -->
<div class="config-sidebar" :class="{ 'is-open': state.isConfigSidebarOpen }">
<h2 class="sidebar-title">
<div class="sidebar-title-text" @click="toggleDebugMode">智能体配置</div>
<div class="toggle-sidebar" @click="toggleConfigSidebar(false)">
<CloseOutlined class="iconfont icon-20" />
</div>
</h2>
<div v-if="selectedAgentId && configSchema" class="config-form">
<!-- 配置表单 -->
<a-form :model="agentConfig" layout="vertical">
<!-- 系统提示词 -->
<div class="empty-config" v-if="state.isEmptyConfig">
<a-alert type="warning" message="该智能体没有配置项" show-icon/>
</div>
<!-- 统一显示所有配置项 -->
<template v-for="(value, key) in configurableItems" :key="key">
<a-form-item
:label="getConfigLabel(key, value)"
:name="key"
class="config-item"
>
<!-- <p>{{ value }}</p> -->
<!-- 根据值的类型使用不同的输入控件 -->
<p v-if="value.description" class="description">{{ value.description }}</p>
<a-switch
v-if="typeof agentConfig[key] === 'boolean'"
v-model:checked="agentConfig[key]"
/>
<a-textarea
v-else-if="key === 'system_prompt'"
v-model:value="agentConfig[key]"
:rows="4"
:placeholder="getPlaceholder(key, value)"
/>
<a-select
v-else-if="value?.options"
v-model:value="agentConfig[key]"
>
<a-select-option v-for="option in value.options" :key="option" :value="option"></a-select-option>
</a-select>
<a-input
v-else
v-model:value="agentConfig[key]"
:placeholder="getPlaceholder(key, value)"
/>
</a-form-item>
</template>
<!-- 保存和重置按钮 -->
<div class="form-actions" v-if="!state.isEmptyConfig">
<a-button type="primary" @click="saveConfig">保存配置</a-button>
<a-button @click="resetConfig">重置</a-button>
</div>
</a-form>
</div>
<div v-else class="no-agent-selected">
请先选择一个智能体
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, reactive, watch, computed, h } from 'vue';
import {
RobotOutlined,
MenuFoldOutlined,
MenuUnfoldOutlined,
CloseOutlined,
SettingOutlined
} from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import AgentChatComponent from '@/components/AgentChatComponent.vue';
// 状态
const agents = ref({});
const selectedAgentId = ref(null);
const state = reactive({
debug_mode: false,
isSidebarOpen: JSON.parse(localStorage.getItem('agent-sidebar-open') || 'true'),
isConfigSidebarOpen: false,
isEmptyConfig: computed(() =>
!selectedAgentId.value ||
Object.keys(configurableItems.value).length === 0
)
});
const configSchema = computed(() => agents.value[selectedAgentId.value]?.config_schema || {});
const configurableItems = computed(() => configSchema.value.configurable_items || {});
// 配置状态
const agentConfig = ref({});
// 调试模式
const toggleDebugMode = () => {
state.debug_mode = !state.debug_mode;
};
// 根据选中的智能体加载配置
const loadAgentConfig = () => {
// BUG: 目前消息重置有问题,需要重置消息
if (!selectedAgentId.value || !agents.value[selectedAgentId.value]) return;
const agent = agents.value[selectedAgentId.value];
const schema = agent.config_schema || {};
const items = schema.configurable_items || {};
// 重置配置
agentConfig.value = {};
// 初始化基础配置项
if (schema.system_prompt) {
agentConfig.value.system_prompt = schema.system_prompt;
}
if (schema.model) {
agentConfig.value.model = schema.model;
}
// 初始化可配置项
Object.keys(items).forEach(key => {
const item = items[key];
// 根据类型设置默认值
if (typeof item.default === 'boolean') {
agentConfig.value[key] = item.default;
} else {
agentConfig.value[key] = item.default || '';
}
});
// 加载存储的配置
const savedConfig = JSON.parse(localStorage.getItem(`agent-config-${selectedAgentId.value}`) || '{}');
// 合并已保存的配置
if (savedConfig) {
Object.keys(savedConfig).forEach(key => {
if (key in agentConfig.value) {
agentConfig.value[key] = savedConfig[key];
}
});
}
};
// 保存配置
const saveConfig = () => {
// 保存配置到本地存储
localStorage.setItem(`agent-config-${selectedAgentId.value}`, JSON.stringify(agentConfig.value));
// 提示保存成功
message.success('配置已保存');
};
// 重置配置
const resetConfig = () => {
// 清除本地存储中的配置
localStorage.removeItem(`agent-config-${selectedAgentId.value}`);
// 重新加载默认配置
loadAgentConfig();
message.info('配置已重置');
};
// 监听侧边栏状态变化并保存到localStorage
watch(
() => state.isSidebarOpen,
(newValue) => {
localStorage.setItem('agent-sidebar-open', JSON.stringify(newValue));
}
);
// 监听智能体选择变化
watch(
() => selectedAgentId.value,
() => {
loadAgentConfig();
}
);
// 获取智能体列表
const fetchAgents = async () => {
try {
const response = await fetch('/api/chat/agent');
if (response.ok) {
const data = await response.json();
// 将数组转换为对象
agents.value = data.agents.reduce((acc, agent) => {
acc[agent.name] = agent;
return acc;
}, {});
console.log("agents", agents.value);
// 加载当前选中智能体的配置
if (selectedAgentId.value) {
loadAgentConfig();
}
} else {
console.error('获取智能体失败');
}
} catch (error) {
console.error('获取智能体错误:', error);
}
};
// 切换左侧侧边栏
const toggleSidebar = () => {
state.isSidebarOpen = !state.isSidebarOpen;
};
// 切换配置侧边栏
const toggleConfigSidebar = (forceOpen) => {
if (forceOpen !== undefined) {
state.isConfigSidebarOpen = forceOpen;
} else {
state.isConfigSidebarOpen = !state.isConfigSidebarOpen;
}
};
// 选择智能体
const selectAgent = (agentId) => {
selectedAgentId.value = agentId;
// 保存选择到本地存储
localStorage.setItem('last-selected-agent', agentId);
// 加载该智能体的配置
loadAgentConfig();
};
// 初始化
onMounted(async () => {
// 获取智能体列表
await fetchAgents();
// 恢复上次选择的智能体
const lastSelectedAgent = localStorage.getItem('last-selected-agent');
if (lastSelectedAgent && agents.value[lastSelectedAgent]) {
selectedAgentId.value = lastSelectedAgent;
} else if (Object.keys(agents.value).length > 0) {
// 默认选择第一个智能体
selectedAgentId.value = Object.keys(agents.value)[0];
}
// 加载配置
loadAgentConfig();
});
// 获取配置标签
const getConfigLabel = (key, value) => {
// 根据配置项属性选择合适的显示文本
if (value.description) {
return `${value.name}${key}`;
}
return key;
};
// 获取占位符
const getPlaceholder = (key, value) => {
// 返回描述作为占位符
return `(默认: ${value.default}` ;
};
</script>
<style lang="less" scoped>
.agent-view {
display: flex;
width: 100%;
height: 100vh;
overflow: hidden;
--agent-sidebar-width: 230px;
--config-sidebar-width: 350px;
}
.sidebar {
width: 0;
max-width: var(--agent-sidebar-width);
border-right: 1px solid var(--main-light-3);
background-color: var(--bg-sider);
box-sizing: content-box;
overflow-y: auto;
transition: width 0.3s ease;
overflow: hidden;
&.is-open {
width: var(--agent-sidebar-width);
}
}
// 配置侧边栏样式
.config-sidebar {
width: 0;
max-width: var(--config-sidebar-width);
border-left: 1px solid var(--main-light-3);
background-color: var(--bg-sider);
box-sizing: content-box;
overflow-y: auto;
transition: width 0.3s ease;
overflow: hidden;
position: relative;
z-index: 100;
&.is-open {
width: var(--config-sidebar-width);
}
.config-form {
padding: 16px;
min-width: calc(var(--config-sidebar-width) - 16px);
overflow-y: auto;
max-height: calc(100vh - 100px);
.description {
font-size: 12px;
color: var(--gray-700);
}
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.no-agent-selected {
padding: 16px;
color: var(--gray-500);
text-align: center;
margin-top: 20px;
}
}
.sidebar-title {
font-weight: bold;
user-select: none;
white-space: nowrap;
overflow: hidden;
padding-bottom: 1rem;
font-size: 1rem;
border-bottom: 1px solid var(--main-light-3);
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
margin: 0;
}
.toggle-sidebar {
cursor: pointer;
&.nav-btn {
height: 2.5rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 8px;
color: var(--gray-900);
cursor: pointer;
font-size: 15px;
width: auto;
padding: 0.5rem 1rem;
transition: background-color 0.3s;
overflow: hidden;
.text {
margin-left: 10px;
}
&:hover {
background-color: var(--main-light-3);
}
.nav-btn-icon {
width: 1.5rem;
height: 1.5rem;
}
}
}
.agent-list {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 1rem;
}
.agent-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: var(--main-light-4);
}
&.active {
background-color: var(--main-light-3);
}
}
.agent-info {
padding: 16px;
min-width: calc(var(--agent-sidebar-width) - 16px);
}
.agent-name {
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.agent-desc {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.content {
flex: 1;
overflow: hidden;
}
// 添加requirements相关样式
.info-section {
margin-top: 16px;
border-top: 1px solid var(--main-light-3);
padding-top: 12px;
h3 {
font-size: 14px;
margin-bottom: 8px;
font-weight: 500;
}
}
.requirements-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
.ant-tag {
user-select: all;
}
}
@media (max-width: 520px) {
.sidebar {
position: absolute;
z-index: 101;
width: 0;
height: 100%;
border-radius: 0 16px 16px 0;
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05);
&.is-open {
width: var(--agent-sidebar-width);
padding: 16px;
}
}
.config-sidebar {
position: absolute;
z-index: 101;
right: 0;
width: 0;
height: 100%;
border-radius: 16px 0 0 16px;
box-shadow: 0 0 10px 1px rgba(0, 0, 0, 0.05);
&.is-open {
width: 90%;
max-width: var(--config-sidebar-width);
}
}
}
</style>