perf: 调整 WebUI sidebar 顺序

This commit is contained in:
Soulter
2025-08-18 11:57:01 +08:00
parent e911896cfb
commit 9e7d46f956
10 changed files with 67 additions and 747 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="93.7287" y1="106.6446" x2="52.9011" y2="81.6944">
<stop offset="0.0969" style="stop-color:#FFB300"/>
<stop offset="1" style="stop-color:#FFB300;stop-opacity:0"/>
</linearGradient>
<path style="fill:url(#SVGID_1_);" d="M123.04,107.67c-4.08-4.12-9.38-9.48-14.92-15.06c-0.34,1.29-0.93,2.39-1.79,3.26
c-6.43,6.43-25.6-1.99-45.31-19.1c-2.46-2.13-16.74,20.28-14.1,22.87c3.27,3.2,26,17.86,33.78,20.73
c22.66,8.35,34.3,0.22,38.24-3.59C121.16,114.61,122.51,111.5,123.04,107.67z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="115.2813" y1="82.3624" x2="14.863" y2="0.8196">
<stop offset="0" style="stop-color:#FFB300"/>
<stop offset="0.7062" style="stop-color:#FDD835"/>
<stop offset="0.8408" style="stop-color:#FDDC36"/>
<stop offset="0.9842" style="stop-color:#FFE93A"/>
<stop offset="1" style="stop-color:#FFEB3B"/>
</linearGradient>
<path style="fill:url(#SVGID_2_);" d="M25.05,27.7c-1.54-4.81-2.88-11.1-0.4-13.5c7.51-7.3,31.69,4.88,54.25,27.43
c22.55,22.55,34.84,46.84,27.43,54.25c-0.07,0.07-0.16,0.13-0.23,0.2c6.13,5.82,12.2,11.6,16.1,15.31
c4.87-14.43-6.45-44.11-31.5-69.96c-4.07-4.2-16.12-16.56-26.55-23.56C54.61,11.47,44.19,5.59,32.57,4.2
C25,3.29,11.45,5.24,14.25,15.98c0.55,2.12,2.31,7.22,8.15,13.3C23.56,30.49,25.56,29.3,25.05,27.7z"/>
<g>
<path style="fill:#FDD835;" d="M55.98,42.1l-0.75,20c-0.06,1.53,0.72,2.98,2.04,3.77l16.86,10.11c1.85,1.25,1.46,4.09-0.66,4.79
L54.79,85.5c-1.51,0.38-2.69,1.57-3.06,3.08l-4.89,19.93c-0.62,2.15-3.43,2.65-4.76,0.85L31.06,92.91
c-0.85-1.26-2.31-1.97-3.83-1.85L7.49,92.61c-2.23,0.07-3.58-2.45-2.28-4.27l12.6-16.19c0.96-1.23,1.16-2.89,0.52-4.31
l-7.88-17.57c-0.76-2.1,1.22-4.17,3.35-3.49l18.39,6.95c1.44,0.54,3.05,0.26,4.22-0.74l15.22-13
C53.39,38.62,55.96,39.87,55.98,42.1z"/>
<g>
<path style="fill:#FFFF8D;" d="M46.99,59.33l4.66-12.75c0.28-0.7,0.7-1.93,1.79-1.4c0.86,0.42,0.46,2.43,0.46,2.43l-1.05,11.54
c-0.41,4.39-1.6,5.38-3.3,5.49C47.6,64.75,45.65,62.98,46.99,59.33z"/>
</g>
<g>
<path style="fill:#F4B400;" d="M53.89,83.73l14.53-3.13c0.73-0.18,2.01-0.42,1.64-1.58c-0.29-0.91-2.34-0.8-2.34-0.8l-10.97-0.86
c-3.21-0.38-5.72,0.14-6.74,1.84C48.65,81.48,49.89,84.32,53.89,83.73z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

@@ -6,12 +6,12 @@
"toolUse": "MCP Tools",
"config": "Config",
"extension": "Extensions",
"extensionMarketplace": "Extension Market",
"chat": "Chat",
"conversation": "Conversations",
"sessionManagement": "Session Management",
"console": "Console",
"alkaid": "Alkaid Lab",
"knowledgeBase": "Knowledge Base",
"about": "About",
"settings": "Settings",
"documentation": "Documentation",
@@ -2,16 +2,16 @@
"dashboard": "统计",
"platforms": "消息平台",
"providers": "服务提供商",
"persona": "人格管理",
"persona": "人格设定",
"toolUse": "MCP",
"extension": "插件",
"config": "配置文件",
"extension": "插件管理",
"extensionMarketplace": "插件市场",
"chat": "聊天",
"conversation": "对话数据",
"conversation": "对话数据",
"sessionManagement": "会话管理",
"console": "控制台",
"alkaid": "Alkaid",
"knowledgeBase": "知识库",
"about": "关于",
"settings": "设置",
"documentation": "官方文档",
@@ -9,6 +9,7 @@ import {useAuthStore} from '@/stores/auth';
import {useCommonStore} from '@/stores/common';
import MarkdownIt from 'markdown-it';
import { useI18n } from '@/i18n/composables';
import { router } from '@/router';
// 配置markdown-it,默认安全设置
const md = new MarkdownIt({
@@ -277,7 +278,7 @@ commonStore.getStartTime();
<v-icon>mdi-menu</v-icon>
</v-btn>
<div class="logo-container" :class="{'mobile-logo': $vuetify.display.xs}">
<div class="logo-container" :class="{'mobile-logo': $vuetify.display.xs}" @click="$router.push('/about')">
<span class="logo-text">Astr<span class="logo-text-light">Bot</span></span>
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
</div>
@@ -610,6 +611,7 @@ commonStore.getStartTime();
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.mobile-logo {
@@ -39,15 +39,25 @@ const sidebarItem: menu[] = [
to: '/tool-use'
},
{
title: 'core.navigation.config',
icon: 'mdi-cog',
to: '/config',
title: 'core.navigation.persona',
icon: 'mdi-heart',
to: '/persona'
},
{
title: 'core.navigation.extension',
icon: 'mdi-puzzle',
to: '/extension'
},
{
title: 'core.navigation.knowledgeBase',
icon: 'mdi-text-box-search',
to: '/alkaid/knowledge-base',
},
{
title: 'core.navigation.config',
icon: 'mdi-cog',
to: '/config',
},
{
title: 'core.navigation.chat',
icon: 'mdi-chat',
@@ -63,26 +73,11 @@ const sidebarItem: menu[] = [
icon: 'mdi-account-group',
to: '/session-management'
},
{
title: 'core.navigation.persona',
icon: 'mdi-heart',
to: '/persona'
},
{
title: 'core.navigation.console',
icon: 'mdi-console',
to: '/console'
},
{
title: 'core.navigation.alkaid',
icon: 'mdi-test-tube',
to: '/alkaid'
},
{
title: 'core.navigation.about',
icon: 'mdi-information',
to: '/about'
},
// {
// title: 'Project ATRI',
// icon: 'mdi-grain',
+25 -20
View File
@@ -66,27 +66,32 @@ const MainRoutes = {
path: '/console',
component: () => import('@/views/ConsolePage.vue')
},
// {
// name: 'Alkaid',
// path: '/alkaid',
// component: () => import('@/views/AlkaidPage.vue'),
// children: [
// {
// path: 'knowledge-base',
// name: 'KnowledgeBase',
// component: () => import('@/views/alkaid/KnowledgeBase.vue')
// },
// {
// path: 'long-term-memory',
// name: 'LongTermMemory',
// component: () => import('@/views/alkaid/LongTermMemory.vue')
// },
// {
// path: 'other',
// name: 'OtherFeatures',
// component: () => import('@/views/alkaid/Other.vue')
// }
// ]
// },
{
name: 'Alkaid',
path: '/alkaid',
component: () => import('@/views/AlkaidPage.vue'),
children: [
{
path: 'knowledge-base',
name: 'KnowledgeBase',
component: () => import('@/views/alkaid/KnowledgeBase.vue')
},
{
path: 'long-term-memory',
name: 'LongTermMemory',
component: () => import('@/views/alkaid/LongTermMemory.vue')
},
{
path: 'other',
name: 'OtherFeatures',
component: () => import('@/views/alkaid/Other.vue')
}
]
name: 'KnowledgeBase',
path: '/alkaid/knowledge-base',
component: () => import('@/views/alkaid/KnowledgeBase.vue')
},
{
name: 'Chat',
+21 -225
View File
@@ -1,94 +1,27 @@
<template>
<v-card style="height: 100%;" elevation="0" class="bg-surface">
<v-card-text style="padding: 0; height: 100%; overflow-y: hidden;">
<div class="about-wrapper">
<!-- Hero Section -->
<section class="hero-section">
<div class="logo-title-container">
<div @click="selectedLogo = selectedLogo == 0 ? 1 : 0" class="logo-container">
<img v-if="selectedLogo == 0" width="280" src="@/assets/images/logo-waifu.png" alt="AstrBot Logo" class="fade-in">
<img v-if="selectedLogo == 1" width="280" src="@/assets/images/logo-normal.svg" alt="AstrBot Logo" class="fade-in">
</div>
<div class="title-container">
<h1 class="text-h2 font-weight-bold">{{ tm('hero.title') }}</h1>
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">{{ tm('hero.subtitle') }}</p>
<div class="action-buttons">
<v-btn @click="open('https://github.com/Soulter/AstrBot')"
color="primary" variant="elevated" prepend-icon="mdi-star">
{{ tm('hero.starButton') }}
</v-btn>
<v-btn class="ml-4" @click="open('https://github.com/Soulter/AstrBot/issues')"
color="secondary" variant="elevated" prepend-icon="mdi-comment-question">
{{ tm('hero.issueButton') }}
</v-btn>
</div>
</div>
</div>
</section>
<!-- Contributors Section -->
<section class="contributors-section">
<v-container>
<v-row justify="center" align="center">
<v-col cols="12" md="6" class="pr-md-8 contributors-info">
<h2 class="text-h4 font-weight-medium">{{ tm('contributors.title') }}</h2>
<p class="mb-4 text-body-1" style="color: var(--v-theme-secondaryText);">
{{ tm('contributors.description') }}
</p>
<p class="text-body-1" style="color: var(--v-theme-secondaryText);">
<a href="https://github.com/Soulter/AstrBot/graphs/contributors" class="text-decoration-none custom-link">{{ tm('contributors.viewLink') }}</a>
</p>
</v-col>
<v-col cols="12" md="6">
<v-card variant="outlined" class="overflow-hidden" elevation="2">
<v-img v-if="useCustomizerStore().uiTheme==='PurpleThemeDark'"
alt="Active Contributors of Soulter/AstrBot"
src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=575865240&limit=365&image_size=auto&color_scheme=dark">
</v-img>
<v-img v-else
alt="Active Contributors of Soulter/AstrBot"
src="https://next.ossinsight.io/widgets/official/compose-recent-active-contributors/thumbnail.png?repo_id=575865240&limit=365&image_size=auto&color_scheme=light">
</v-img>
</v-card>
</v-col>
</v-row>
</v-container>
</section>
<!-- Stats Section -->
<section class="stats-section">
<v-container>
<v-row justify="center" align="center" class="flex-md-row-reverse">
<v-col cols="12" md="6" class="pl-md-8 stats-info">
<h2 class="text-h4 font-weight-medium">{{ tm('stats.title') }}</h2>
<div class="license-container mt-8">
<img v-bind="props" src="https://www.gnu.org/graphics/agplv3-with-text-100x42.png" style="cursor: pointer;"/>
<p class="text-caption mt-2" style="color: var(--v-theme-secondaryText);">{{ tm('stats.license') }}</p>
</div>
</v-col>
<v-col cols="12" md="6">
<v-card variant="outlined" class="overflow-hidden" elevation="2">
<v-img v-if="useCustomizerStore().uiTheme==='PurpleThemeDark'"
alt="Stars Map of Soulter/AstrBot"
src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-map/thumbnail.png?activity=stars&repo_id=575865240&image_size=auto&color_scheme=dark">
</v-img>
<v-img v-else
alt="Stars Map of Soulter/AstrBot"
src="https://next.ossinsight.io/widgets/official/analyze-repo-stars-map/thumbnail.png?activity=stars&repo_id=575865240&image_size=auto&color_scheme=light">
</v-img>
</v-card>
</v-col>
</v-row>
</v-container>
</section>
<div style="display: flex; flex-direction: column; height: 100%;">
<div style="flex-grow: 1; display: flex; align-items: center; justify-content: center; flex-direction: column;">
<div style="text-align: center; max-width: 600px;">
<h1 class="font-weight-bold">{{ tm('hero.title') }}</h1>
<p class="text-subtitle-1" style="color: var(--v-theme-secondaryText);">{{ tm('hero.subtitle') }}</p>
<div style="margin-top: 20px; display: flex; justify-content: center;">
<v-btn @click="open('https://github.com/Soulter/AstrBot')" color="primary" variant="tonal"
prepend-icon="mdi-star">
{{ tm('hero.starButton') }}
</v-btn>
<v-btn class="ml-4" @click="open('https://github.com/Soulter/AstrBot/issues')" color="secondary"
variant="tonal" prepend-icon="mdi-comment-question">
{{ tm('hero.issueButton') }}
</v-btn>
</div>
</div>
</v-card-text>
</v-card>
</div>
</div>
</template>
<script>
import {useCustomizerStore} from "@/stores/customizer";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
export default {
@@ -97,148 +30,11 @@ export default {
const { tm } = useModuleI18n('features/about');
return { tm };
},
data() {
return {
selectedLogo: 0
}
},
methods: {
useCustomizerStore,
useCustomizerStore,
open(url) {
window.open(url, '_blank');
}
}
}
</script>
<style scoped>
.about-wrapper {
min-height: 100%;
}
.hero-section {
padding: 40px 20px;
background: linear-gradient(to right bottom, rgba(255,255,255,0.7), rgba(240,240,250,0.3));
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.logo-title-container {
display: flex;
align-items: center;
flex-direction: row;
max-width: 900px;
gap: 20px;
}
.logo-container {
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
}
.logo-container:hover {
transform: scale(1.05);
}
.title-container {
text-align: left;
}
.contributors-section, .stats-section {
padding: 60px 20px;
}
.contributors-section {
background-color: var(--v-theme-containerBg, #f9f9fb);
}
.contributors-info, .stats-info {
display: flex;
flex-direction: column;
justify-content: center;
}
.custom-link {
display: inline-block;
padding: 5px 0;
position: relative;
color: var(--v-primary-base);
font-weight: 500;
}
.custom-link::after {
content: '';
position: absolute;
width: 100%;
transform: scaleX(0);
height: 2px;
bottom: 0;
left: 0;
background-color: var(--v-primary-base);
transform-origin: bottom right;
transition: transform 0.25s ease-out;
}
.custom-link:hover::after {
transform: scaleX(1);
transform-origin: bottom left;
}
.license-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.action-buttons {
display: flex;
margin-top: 24px;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.2s ease-in-out;
}
@media (max-width: 960px) {
.logo-title-container {
flex-direction: column;
text-align: center;
}
.title-container {
text-align: center;
}
.action-buttons {
justify-content: center;
}
.license-container {
align-items: center;
}
.contributors-section, .stats-section {
padding: 40px 20px;
}
}
@media (max-width: 600px) {
.action-buttons {
flex-direction: column;
gap: 12px;
}
.action-buttons .v-btn + .v-btn {
margin-left: 0 !important;
}
}
</style>
</script>
-438
View File
@@ -1,438 +0,0 @@
<script setup>
import Graph from "graphology";
import Sigma from "sigma";
import ForceSupervisor from "graphology-layout-force/worker";
</script>
<template>
<v-card style="height: 100%; width: 100%;">
<v-card-text class="pa-4" style="height: 100%;">
<v-container fluid class="d-flex flex-column" style="height: 100%;">
<div style="margin-bottom: 32px;">
<h1 class="gradient-text">The Alkaid Project.</h1>
<small style="color: #a3a3a3;">{{ tm('features.alkaid.index.sigma.subtitle') }}</small>
</div>
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<v-btn size="large" :variant="activeTab === 'long-term-memory' ? 'flat' : 'tonal'"
:color="activeTab === 'long-term-memory' ? '#9b72cb' : ''" rounded="lg"
@click="activeTab = 'long-term-memory'">
<v-icon start>mdi-dots-hexagon</v-icon>
长期记忆层
</v-btn>
<v-btn size="large" :variant="activeTab === 'other' ? 'flat' : 'tonal'"
:color="activeTab === 'other' ? '#9b72cb' : ''" rounded="lg" @click="activeTab = 'other'">
<v-icon start>mdi-dots-horizontal</v-icon>
其他
</v-btn>
</div>
<div v-if="activeTab === 'long-term-memory'" id="long-term-memory" class="flex-grow-1"
style="display: flex; flex-direction: row;">
<div id="graph-container" style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
</div>
<div id="graph-control-panel"
style="min-width: 450px; border: 1px solid #eee; border-radius: 8px; padding: 16px; margin-left: 16px;">
<div>
<span style="color: #333333;">{{ tm('features.alkaid.index.sigma.visualization') }}</span>
<div style="margin-top: 8px;">
<v-autocomplete v-model="searchUserId" :items="userIdList" variant="outlined"
:label="tm('features.alkaid.index.sigma.filterUserId')"></v-autocomplete>
<v-btn color="primary" @click="onNodeSelect" variant="tonal" style="margin-top: 8px;">
<v-icon start>mdi-magnify</v-icon>
{{ tm('features.alkaid.index.sigma.filter') }}
</v-btn>
<v-btn color="secondary" @click="resetFilter" variant="tonal"
style="margin-top: 8px; margin-left: 8px;">
<v-icon start>mdi-filter-remove</v-icon>
{{ tm('features.alkaid.index.sigma.resetFilter') }}
</v-btn>
</div>
<div style="margin-top: 16px;">
<v-btn color="primary" @click="refreshGraph" variant="tonal">
<v-icon start>mdi-refresh</v-icon>
{{ tm('features.alkaid.index.sigma.refreshGraph') }}
</v-btn>
</div>
</div>
<v-divider class="my-4"></v-divider>
<div v-if="selectedNode" class="mt-4">
<h3>{{ tm('features.alkaid.index.sigma.nodeDetails') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div v-if="selectedNode.id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.id') }}:</span>
<span>{{ selectedNode.id }}</span>
</div>
</div>
<div v-if="selectedNode._label">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.type') }}:</span>
<span>{{ selectedNode._label }}</span>
</div>
</div>
<div v-if="selectedNode.name">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.name') }}:</span>
<span>{{ selectedNode.name }}</span>
</div>
</div>
<div v-if="selectedNode.user_id">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.userId') }}:</span>
<span>{{ selectedNode.user_id }}</span>
</div>
</div>
<div v-if="selectedNode.ts">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.timestamp') }}:</span>
<span>{{ selectedNode.ts }}</span>
</div>
</div>
<div v-if="selectedNode.type">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.type') }}:</span>
<span>{{ selectedNode.type }}</span>
</div>
</div>
</v-card>
</div>
<div v-if="graphStats" class="mt-4">
<h3>{{ tm('features.alkaid.index.sigma.graphStats') }}</h3>
<v-card variant="outlined" class="mt-2 pa-3">
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.nodeCount') }}:</span>
<span>{{ graphStats.nodeCount }}</span>
</div>
<div class="d-flex justify-space-between">
<span class="text-subtitle-2">{{ tm('features.alkaid.index.sigma.edgeCount') }}:</span>
<span>{{ graphStats.edgeCount }}</span>
</div>
</v-card>
</div>
</div>
</div>
<div v-if="activeTab === 'other'" class="flex-grow-1" style="display: flex; flex-direction: column;">
<div class="d-flex align-center justify-center"
style="flex-grow: 1; width: 100%; border: 1px solid #eee; border-radius: 8px;">
<v-icon size="64" color="grey-lighten-1">mdi-tools</v-icon>
<p class="text-h6 text-grey ml-4">{{ tm('features.alkaid.index.sigma.inDevelopment') }}</p>
</div>
</div>
</v-container>
</v-card-text>
</v-card>
</template>
<script>
import axios from 'axios';
import AstrBotConfig from '@/components/shared/AstrBotConfig.vue';
import WaitingForRestart from '@/components/shared/WaitingForRestart.vue';
import { useModuleI18n } from '@/i18n/composables';
export default {
name: 'AlkaidPage',
components: {
AstrBotConfig,
WaitingForRestart
},
setup() {
const { tm } = useModuleI18n('features/alkaid/index');
return { tm };
},
data() {
return {
renderer: null,
graph: null,
layout: null,
activeTab: 'long-term-memory',
node_data: [],
edge_data: [],
searchUserId: null,
userIdList: [],
selectedNode: null,
graphStats: null,
nodeColors: {
'PhaseNode': '#4CAF50', // 绿色
'PassageNode': '#2196F3', // 蓝色
'FactNode': '#FF9800', // 橙色
'default': '#9C27B0' // 紫色作为默认
},
edgeColors: {
'_include_': '#607D8B',
'_related_': '#9E9E9E',
'default': '#BDBDBD'
},
isLoading: false
}
},
mounted() {
this.initSigma();
this.ltmGetGraph();
this.ltmGetUserIds();
},
beforeUnmount() {
if (this.renderer) {
this.renderer.kill();
}
if (this.layout) {
this.layout.stop();
}
},
watch: {
activeTab(newVal) {
if (newVal === 'long-term-memory') {
this.$nextTick(() => {
if (!this.renderer) {
this.initSigma();
}
});
} else {
if (this.renderer) {
this.renderer.kill();
this.renderer = null;
}
if (this.layout) {
this.layout.stop();
this.layout = null;
}
}
}
},
methods: {
ltmGetGraph(userId = null) {
this.isLoading = true;
const params = userId ? { user_id: userId } : {};
axios.get('/api/plug/alkaid/ltm/graph', { params })
.then(response => {
let nodes = response.data.data.nodes;
let edges = response.data.data.edges;
this.node_data = nodes;
this.edge_data = edges;
if (this.graph) {
this.graph.clear();
}
nodes.forEach(node => {
const nodeId = node[0];
const nodeData = node[1];
if (!this.graph.hasNode(nodeId)) {
const nodeType = nodeData._label || 'default';
const color = this.nodeColors[nodeType] || this.nodeColors['default'];
this.graph.addNode(nodeId, {
x: Math.random(),
y: Math.random(),
size: 5,
label: nodeData.name || nodeId.split('_')[0],
color: color,
originalData: nodeData
});
}
});
// 添加边
edges.forEach(edge => {
const sourceId = edge[0];
const targetId = edge[1];
const edgeData = edge[2];
if (this.graph.hasNode(sourceId) && this.graph.hasNode(targetId)) {
const edgeId = `${sourceId}->${targetId}`;
const relationType = edgeData.relation_type || 'default';
const color = this.edgeColors[relationType] || this.edgeColors['default'];
this.graph.addEdge(sourceId, targetId, {
size: 1,
color: color,
originalData: edgeData,
label: relationType,
type: "line"
});
} else {
console.warn(`Edge ${sourceId} -> ${targetId} has missing nodes.`);
}
});
this.updateGraphStats();
console.log('Graph initialized with', nodes.length, 'nodes and', edges.length, 'edges');
})
.catch(error => {
console.error('Error fetching graph data:', error);
})
.finally(() => {
this.isLoading = false;
});
if (this.layout) {
this.layout.start();
}
},
ltmGetUserIds() {
axios.get('/api/plug/alkaid/ltm/user_ids')
.then(response => {
this.userIdList = response.data.data;
})
.catch(error => {
console.error('Error fetching user IDs:', error);
});
},
updateGraphStats() {
if (this.graph) {
this.graphStats = {
nodeCount: this.graph.order,
edgeCount: this.graph.size
};
}
},
refreshGraph() {
this.ltmGetGraph(this.searchUserId);
},
onNodeSelect() {
console.log('Selected user ID:', this.searchUserId);
if (!this.searchUserId || !this.graph) return;
// 使用API的user_id参数筛选数据
this.ltmGetGraph(this.searchUserId);
},
resetFilter() {
this.searchUserId = null;
this.ltmGetGraph();
},
initSigma() {
const container = document.getElementById("graph-container");
if (!container) return;
if (this.renderer) {
this.renderer.kill();
this.renderer = null;
}
if (this.layout) {
this.layout.stop();
this.layout = null;
}
const graph = new Graph({
multi: true,
});
const layout = new ForceSupervisor(graph, {
isNodeFixed: (_, attr) => attr.highlighted, settings: {
gravity: 0.0001,
repulsion: 0.001
}
});
layout.start();
this.layout = layout;
this.graph = graph;
const renderer = new Sigma(graph, container, {
minCameraRatio: 0.01,
maxCameraRatio: 2,
labelRenderedSizeThreshold: 1,
renderLabels: true,
renderEdgeLabels: true,
labelSize: 14,
labelColor: "#333333",
});
this.renderer = renderer;
let draggedNode = null;
let isDragging = false;
renderer.on("downNode", (e) => {
isDragging = true;
draggedNode = e.node;
graph.setNodeAttribute(draggedNode, "highlighted", true);
if (!renderer.getCustomBBox()) renderer.setCustomBBox(renderer.getBBox());
});
renderer.on("moveBody", ({ event }) => {
if (!isDragging || !draggedNode) return;
const pos = renderer.viewportToGraph(event);
graph.setNodeAttribute(draggedNode, "x", pos.x);
graph.setNodeAttribute(draggedNode, "y", pos.y);
event.preventSigmaDefault();
event.original.preventDefault();
event.original.stopPropagation();
});
const handleUp = () => {
if (draggedNode) {
graph.removeNodeAttribute(draggedNode, "highlighted");
}
isDragging = false;
draggedNode = null;
};
renderer.on("upNode", handleUp);
renderer.on("upStage", handleUp);
renderer.on("clickNode", (e) => {
const nodeId = e.node;
const nodeAttributes = graph.getNodeAttributes(nodeId);
this.selectedNode = nodeAttributes.originalData;
});
renderer.on("clickStage", () => {
this.selectedNode = null;
});
},
getRandomColor() {
const letters = '0123456789ABCDEF';
let color = '#';
for (let i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
},
}
</script>
<style scoped>
.gradient-text {
background: linear-gradient(74deg, #2abfe1 0, #9b72cb 25%, #b55908 50%, #d93025 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
font-weight: bold;
}
#graph-container {
position: relative;
background-color: #f2f6f9;
overflow: hidden;
min-height: 200px;
}
#graph-container:hover {
cursor: pointer;
}
.memory-header {
padding: 0 8px;
}
</style>