ForcePilot/web/src/layouts/AppLayout.vue
Wenjie Zhang 8087ef4b00 feat(chatbot): 增强智能体工具和知识库集成能力
- 在智能体上下文中添加知识库字段,支持从知识库中选择工具
- 重构工具获取逻辑,支持根据上下文动态加载工具
- 优化前端知识库创建和加载逻辑,移入 store 管理
- 改进工具调用结果渲染,支持知识库工具识别
- 添加 clear-btn 样式改进
2025-12-30 19:01:45 +08:00

598 lines
13 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.

<script setup>
import { ref, reactive, onMounted, useTemplateRef, computed, provide } from 'vue'
import { RouterLink, RouterView, useRoute } from 'vue-router'
import { GithubOutlined } from '@ant-design/icons-vue'
import { Bot, Waypoints, LibraryBig, BarChart3, CircleCheck } from 'lucide-vue-next';
import { onLongPress } from '@vueuse/core'
import { useConfigStore } from '@/stores/config'
import { useDatabaseStore } from '@/stores/database'
import { useInfoStore } from '@/stores/info'
import { useTaskerStore } from '@/stores/tasker'
import { storeToRefs } from 'pinia'
import UserInfoComponent from '@/components/UserInfoComponent.vue'
import DebugComponent from '@/components/DebugComponent.vue'
import TaskCenterDrawer from '@/components/TaskCenterDrawer.vue'
import SettingsModal from '@/components/SettingsModal.vue'
const configStore = useConfigStore()
const databaseStore = useDatabaseStore()
const infoStore = useInfoStore()
const taskerStore = useTaskerStore()
const { activeCount: activeCountRef, isDrawerOpen } = storeToRefs(taskerStore)
const layoutSettings = reactive({
showDebug: false,
useTopBar: false, // 是否使用顶栏
})
// Add state for GitHub stars
const githubStars = ref(0)
const isLoadingStars = ref(false)
// Add state for debug modal
const showDebugModal = ref(false)
const htmlRefHook = useTemplateRef('htmlRefHook')
// Add state for settings modal
const showSettingsModal = ref(false)
// Provide settings modal methods to child components
const openSettingsModal = () => {
showSettingsModal.value = true
}
// Setup long press for debug modal
onLongPress(
htmlRefHook,
() => {
console.log('long press')
showDebugModal.value = true
},
{
delay: 1000, // 1秒长按
modifiers: {
prevent: true
}
}
)
// Handle debug modal close
const handleDebugModalClose = () => {
showDebugModal.value = false
}
const getRemoteConfig = () => {
configStore.refreshConfig()
}
const getRemoteDatabase = () => {
databaseStore.loadDatabases()
}
// Fetch GitHub stars count
const fetchGithubStars = async () => {
try {
isLoadingStars.value = true
// 公共API可以直接使用fetch
const response = await fetch('https://api.github.com/repos/xerrors/Yuxi-Know')
const data = await response.json()
githubStars.value = data.stargazers_count
} catch (error) {
console.error('获取GitHub stars失败:', error)
} finally {
isLoadingStars.value = false
}
}
onMounted(async () => {
// 加载信息配置
await infoStore.loadInfoConfig()
// 加载其他配置
getRemoteConfig()
getRemoteDatabase()
fetchGithubStars() // Fetch GitHub stars on mount
// 预加载任务数据,确保任务中心打开时有内容
taskerStore.loadTasks()
})
// 打印当前页面的路由信息,使用 vue3 的 setup composition API
const route = useRoute()
console.log(route)
const activeTaskCount = computed(() => activeCountRef.value || 0)
// 下面是导航菜单部分,添加智能体项
const mainList = [{
name: '智能体',
path: '/agent',
icon: Bot,
activeIcon: Bot,
}, {
name: '图谱',
path: '/graph',
icon: Waypoints,
activeIcon: Waypoints,
}, {
name: '知识库',
path: '/database',
icon: LibraryBig,
activeIcon: LibraryBig,
}, {
name: 'Dashboard',
path: '/dashboard',
icon: BarChart3,
activeIcon: BarChart3,
}
]
// Provide settings modal methods to child components
provide('settingsModal', {
openSettingsModal
})
</script>
<template>
<div class="app-layout" :class="{ 'use-top-bar': layoutSettings.useTopBar }">
<div class="header" :class="{ 'top-bar': layoutSettings.useTopBar }">
<div class="logo circle">
<router-link to="/">
<img :src="infoStore.organization.avatar">
</router-link>
</div>
<div class="nav">
<!-- 使用mainList渲染导航项 -->
<RouterLink
v-for="(item, index) in mainList"
:key="index"
:to="item.path"
v-show="!item.hidden"
class="nav-item"
active-class="active">
<a-tooltip placement="right">
<template #title>{{ item.name }}</template>
<component class="icon" :is="route.path.startsWith(item.path) ? item.activeIcon : item.icon" size="22"/>
</a-tooltip>
</RouterLink>
<div
class="nav-item task-center"
:class="{ active: isDrawerOpen }"
@click="taskerStore.openDrawer()"
>
<a-tooltip placement="right">
<template #title>任务中心</template>
<a-badge
:count="activeTaskCount"
:overflow-count="99"
class="task-center-badge"
size="small"
>
<CircleCheck class="icon" size="22" />
</a-badge>
</a-tooltip>
</div>
</div>
<div
ref="htmlRefHook"
class="fill debug-trigger"
></div>
<div class="github nav-item">
<a-tooltip placement="right">
<template #title>欢迎 Star</template>
<a href="https://github.com/xerrors/Yuxi-Know" target="_blank" class="github-link">
<GithubOutlined class="icon" />
<span v-if="githubStars > 0" class="github-stars">
<span class="star-count">{{ (githubStars / 1000).toFixed(1) }}k</span>
</span>
</a>
</a-tooltip>
</div>
<!-- <div class="nav-item api-docs">
<a-tooltip placement="right">
<template #title>接口文档 {{ apiDocsUrl }}</template>
<a :href="apiDocsUrl" target="_blank" class="github-link">
<ApiOutlined class="icon" style="color: var(--gray-1000);"/>
</a>
</a-tooltip>
</div> -->
<!-- 用户信息组件 -->
<div class="nav-item user-info">
<UserInfoComponent />
</div>
</div>
<div class="header-mobile">
<RouterLink to="/agent" class="nav-item" active-class="active">对话</RouterLink>
<RouterLink to="/database" class="nav-item" active-class="active">知识</RouterLink>
</div>
<router-view v-slot="{ Component, route }" id="app-router-view">
<keep-alive v-if="route.meta.keepAlive !== false">
<component :is="Component" />
</keep-alive>
<component :is="Component" v-else />
</router-view>
<!-- Debug Modal -->
<a-modal
v-model:open="showDebugModal"
title="调试面板"
width="90%"
:footer="null"
@cancel="handleDebugModalClose"
:maskClosable="true"
:destroyOnClose="true"
class="debug-modal"
>
<DebugComponent />
</a-modal>
<TaskCenterDrawer />
<SettingsModal
v-model:visible="showSettingsModal"
@close="() => showSettingsModal = false"
/>
</div>
</template>
<style lang="less" scoped>
// Less 变量定义
@header-width: 50px;
.app-layout {
display: flex;
flex-direction: row;
width: 100%;
height: 100vh;
min-width: var(--min-width);
.header-mobile {
display: none;
}
.debug-panel {
position: absolute;
z-index: 100;
right: 0;
bottom: 50px;
border-radius: 20px 0 0 20px;
cursor: pointer;
}
}
div.header, #app-router-view {
height: 100%;
max-width: 100%;
user-select: none;
}
#app-router-view {
flex: 1 1 auto;
overflow-y: auto;
}
.header {
display: flex;
flex-direction: column;
flex: 0 0 @header-width;
justify-content: flex-start;
align-items: center;
background-color: var(--main-0);
height: 100%;
width: @header-width;
border-right: 1px solid var(--gray-100);
.nav {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
// height: 45px;
gap: 16px;
}
// 添加debug触发器样式
.debug-trigger {
position: relative;
height: 100%;
width: 100%;
min-height: 20px;
flex-grow: 1;
}
.logo {
width: 34px;
height: 34px;
margin: 12px 0 20px 0;
img {
width: 100%;
height: 100%;
border-radius: 4px; // 50% for circle
}
& > a {
text-decoration: none;
font-size: 24px;
font-weight: bold;
color: var(--gray-900);
}
}
.nav-item {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 4px;
border: 1px solid transparent;
border-radius: 12px;
background-color: transparent;
color: var(--gray-1000);
font-size: 20px;
transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out;
margin: 0;
text-decoration: none;
cursor: pointer;
outline: none;
& > svg:focus {
outline: none;
}
& > svg:focus-visible {
outline: none;
}
&.active {
background-color: var(--gray-100);
font-weight: bold;
color: var(--main-color);
}
&.warning {
color: var(--color-error-500);
}
&:hover {
color: var(--main-color);
}
&.github {
padding: 10px 12px;
margin-bottom: 16px;
&:hover {
background-color: transparent;
border: 1px solid transparent;
}
.github-link {
display: flex;
flex-direction: column;
align-items: center;
color: inherit;
}
.github-stars {
display: flex;
align-items: center;
font-size: 12px;
margin-top: 4px;
.star-icon {
color: var(--color-warning-500);
font-size: 12px;
margin-right: 2px;
}
.star-count {
font-weight: 600;
}
}
}
&.api-docs {
padding: 10px 12px;
}
&.docs { display: none; }
&.task-center {
.task-center-badge {
width: 100%;
display: flex;
justify-content: center;
}
}
&.theme-toggle-nav {
.theme-toggle-icon {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
cursor: pointer;
color: var(--gray-1000);
transition: color 0.2s ease-in-out;
&:hover {
color: var(--main-color);
}
}
}
&.user-info {
margin-bottom: 8px;
}
}
}
@media (max-width: 520px) {
.app-layout {
flex-direction: column-reverse;
div.header {
display: none;
}
.debug-panel {
bottom: 10rem;
}
}
.app-layout div.header-mobile {
display: flex;
flex-direction: row;
width: 100%;
padding: 0 20px;
justify-content: space-around;
align-items: center;
flex: 0 0 60px;
border-right: none;
height: 40px;
.nav-item {
text-decoration: none;
width: 40px;
color: var(--gray-900);
font-size: 1rem;
font-weight: bold;
transition: color 0.1s ease-in-out, font-size 0.1s ease-in-out;
&.active {
color: var(--gray-10000);
font-size: 1.1rem;
}
}
}
.app-layout .chat-box::webkit-scrollbar {
width: 0;
}
}
.app-layout.use-top-bar {
flex-direction: column;
}
.header.top-bar {
flex-direction: row;
flex: 0 0 50px;
width: 100%;
height: 50px;
border-right: none;
border-bottom: 1px solid var(--main-40);
background-color: var(--main-20);
padding: 0 20px;
gap: 24px;
.logo {
width: fit-content;
height: 28px;
margin-right: 16px;
display: flex;
align-items: center;
a {
display: flex;
align-items: center;
text-decoration: none;
color: inherit;
}
img {
width: 28px;
height: 28px;
margin-right: 8px;
}
}
.nav {
flex-direction: row;
height: auto;
gap: 20px;
}
.nav-item {
flex-direction: row;
width: auto;
padding: 4px 16px;
margin: 0;
.icon {
margin-right: 8px;
font-size: 15px; // 减小图标大小
border: none;
outline: none;
&:focus, &:active {
border: none;
outline: none;
}
}
.text {
margin-top: 0;
font-size: 15px;
}
&.github {
padding: 8px 12px;
.icon {
margin-right: 0;
font-size: 18px;
}
&.active {
color: var(--main-color);
}
a {
display: flex;
align-items: center;
}
.github-stars {
display: flex;
align-items: center;
margin-left: 6px;
.star-icon {
color: var(--color-warning-500);
font-size: 14px;
margin-right: 2px;
}
}
}
&.theme-toggle-nav {
padding: 8px 12px;
.theme-toggle-icon {
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-1000);
transition: color 0.2s ease-in-out;
cursor: pointer;
&:hover {
color: var(--main-color);
}
}
&.active {
.theme-toggle-icon {
color: var(--main-color);
}
}
}
}
}
</style>