feat(index): add homepage with dashboard cards and knowledge graph support
- Add new Index view with dashboard layout and quick action cards - Create TopCardList component to display core data management options - Add GuideCard and VersionCard components for user guidance - Add QuickActions component for common operations - Create KnowledgeGraph and KnowledgeGraphCard components for knowledge base visualization - Add comprehensive SVG assets for index page (apps, arrows, management icons) - Add common API module for shared request utilities - Extend knowledgeBase API with knowledge graph endpoints (getKnowledgeGraph, getKnowledgeGraphEntityTypes) - Update i18n translations for English and Chinese with new index page strings - Update routing configuration to include new Index route - Update menu configuration to reflect new navigation structure - Update KnowledgeBase CreateModal and Private view components - Add TypeScript types for Index page components - Improve overall UI/UX with new dashboard-style homepage
This commit is contained in:
@@ -2,18 +2,18 @@
|
||||
import { useEffect, useState, useRef, useCallback, type FC } from 'react';
|
||||
import { useNavigate, useParams, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Switch, Button, Dropdown, Space, Modal, message } from 'antd';
|
||||
import { Switch, Button, Dropdown, Space, Modal, message, Radio } from 'antd';
|
||||
import type { MenuProps } from 'antd';
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import Table, { type TableRef } from '@/components/Table'
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { MoreOutlined, DeploymentUnitOutlined, BarsOutlined } from '@ant-design/icons';
|
||||
import folderIcon from '@/assets/images/knowledgeBase/folder.png';
|
||||
import textIcon from '@/assets/images/knowledgeBase/text.png';
|
||||
import editIcon from '@/assets/images/knowledgeBase/edit.png';
|
||||
import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png';
|
||||
import imageIcon from '@/assets/images/knowledgeBase/image.png'
|
||||
// import blankIcon from '@/assets/images/knowledgeBase/blankDocument.png';
|
||||
// import imageIcon from '@/assets/images/knowledgeBase/image.png'
|
||||
import { getKnowledgeBaseDetail, deleteDocument, downloadFile, updateKnowledgeBase } from '@/api/knowledgeBase';
|
||||
import {
|
||||
type CreateModalRef,
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
type CreateFolderModalRef,
|
||||
type CreateSetModalRef,
|
||||
type ShareModalRef,
|
||||
type CreateDatasetModalRef,type FolderFormData,
|
||||
type KnowledgeBaseDocumentData,
|
||||
type CreateDatasetModalRef,
|
||||
type FolderFormData,
|
||||
type KnowledgeBaseDocumentData,
|
||||
type KnowledgeBaseFormData,
|
||||
} from '@/views/KnowledgeBase/types';
|
||||
import RecallTestDrawer from '../components/RecallTestDrawer';
|
||||
import CreateFolderModal from '../components/CreateFolderModal';
|
||||
@@ -34,7 +36,7 @@ import CreateDatasetModal from '../components/CreateDatasetModal';
|
||||
import CreateImageDataset from '../components/CreateImageDataset';
|
||||
import FolderTree, { type TreeNodeData } from '../components/FolderTree';
|
||||
import { formatDateTime } from '@/utils/format';
|
||||
|
||||
import KnowledgeGraphCard from '../components/KnowledgeGraphCard';
|
||||
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
|
||||
import './Private.css'
|
||||
const { confirm } = Modal
|
||||
@@ -68,7 +70,7 @@ const Private: FC = () => {
|
||||
const datasetModalRef = useRef<CreateDatasetModalRef>(null);
|
||||
const [folderTreeRefreshKey, setFolderTreeRefreshKey] = useState(0);
|
||||
const [autoExpandPath, setAutoExpandPath] = useState<Array<{ id: string; name: string }>>([]);
|
||||
|
||||
const [isGraph, setIsGraph] = useState(false);
|
||||
const { updateBreadcrumbs } = useBreadcrumbManager({
|
||||
breadcrumbType: 'detail',
|
||||
// 不提供 onKnowledgeBaseMenuClick,让它使用默认的导航行为(返回列表页面)
|
||||
@@ -376,9 +378,37 @@ const Private: FC = () => {
|
||||
|
||||
// 处理开关
|
||||
const onChange = (checked: boolean) => {
|
||||
updateKnowledgeBase(knowledgeBaseId || '', {
|
||||
if (!knowledgeBase) return;
|
||||
|
||||
// 构造完整的更新数据,保留现有配置
|
||||
const updateData: KnowledgeBaseFormData = {
|
||||
name: knowledgeBase.name,
|
||||
description: knowledgeBase.description,
|
||||
embedding_id: knowledgeBase.embedding_id,
|
||||
llm_id: knowledgeBase.llm_id,
|
||||
image2text_id: knowledgeBase.image2text_id,
|
||||
reranker_id: knowledgeBase.reranker_id,
|
||||
permission_id: knowledgeBase.permission_id,
|
||||
type: knowledgeBase.type,
|
||||
status: checked ? 1 : 0,
|
||||
});
|
||||
parser_config: knowledgeBase.parser_config || {
|
||||
chunk_token_num: 512,
|
||||
delimiter: '\n',
|
||||
auto_keywords: 0,
|
||||
auto_questions: 0,
|
||||
html4excel: false,
|
||||
graphrag: {
|
||||
use_graphrag: false,
|
||||
scene_name: '',
|
||||
entity_types: '',
|
||||
method: '',
|
||||
resolution: false,
|
||||
community: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateKnowledgeBase(knowledgeBaseId || '', updateData);
|
||||
console.log(`switch to ${checked}`);
|
||||
};
|
||||
// 处理搜索
|
||||
@@ -626,17 +656,15 @@ const Private: FC = () => {
|
||||
}
|
||||
|
||||
const handleRefreshTable = () => {
|
||||
debugger
|
||||
// 刷新表格数据
|
||||
tableRef.current?.loadData();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<div className="rb:flex rb:h-full rb:gap-4">
|
||||
{folder && (
|
||||
<div className="rb:w-80 rb:flex-shrink-0 rb:h-[calc(100%+40px)] rb:mt-[-16px] rb:border-r rb:border-[#EAECEE] rb:p-4 rb:bg-transparent">
|
||||
<div className="rb:w-64 rb:flex-shrink-0 rb:h-[calc(100%+40px)] rb:mt-[-16px] rb:border-r rb:border-[#EAECEE] rb:p-4 rb:bg-transparent">
|
||||
<FolderTree
|
||||
multiple
|
||||
className="customTree"
|
||||
@@ -678,6 +706,14 @@ const Private: FC = () => {
|
||||
<div className='rb:flex rb:items-center rb:justify-between rb:mb-4'>
|
||||
<SearchInput placeholder={t('knowledgeBase.search')} onSearch={handleSearch} />
|
||||
<div className='rb:flex-1 rb:flex rb:items-center rb:justify-end rb:gap-2.5'>
|
||||
<Radio.Group value={isGraph} onChange={(e) => setIsGraph(e.target.value)}>
|
||||
<Radio.Button value={false} >
|
||||
<BarsOutlined />
|
||||
</Radio.Button>
|
||||
<Radio.Button value={true} >
|
||||
<DeploymentUnitOutlined />
|
||||
</Radio.Button>
|
||||
</Radio.Group>
|
||||
<Button onClick={handleShare}>{t('knowledgeBase.share')}</Button>
|
||||
<Button onClick={handleRecallTest}>{t('knowledgeBase.recallTest')}</Button>
|
||||
<Button onClick={handleSetting}>{t('knowledgeBase.knowledgeBase')} {t('knowledgeBase.setting')}</Button>
|
||||
@@ -688,14 +724,18 @@ const Private: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:rounded rb:max-h-[calc(100%-100px)] rb:overflow-y-auto">
|
||||
<Table
|
||||
ref={tableRef}
|
||||
apiUrl={tableApi}
|
||||
apiParams={query as Record<string, unknown>}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
scrollX={1500}
|
||||
/>
|
||||
{isGraph ? (
|
||||
<KnowledgeGraphCard knowledgeBaseId={knowledgeBase.id} />
|
||||
) : (
|
||||
<Table
|
||||
ref={tableRef}
|
||||
apiUrl={tableApi}
|
||||
apiParams={query as Record<string, unknown>}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
scrollX={1500}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RecallTestDrawer
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import { Form, Input, Select, Modal } from 'antd';
|
||||
import { Form, Input, Select, Modal, Tabs, Switch, Radio, Button,message } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { KnowledgeBaseListItem, KnowledgeBaseFormData, CreateModalRef, CreateModalRefProps } from '@/views/KnowledgeBase/types';
|
||||
import { getModelTypeList, getModelList, createKnowledgeBase, updateKnowledgeBase } from '@/api/knowledgeBase'
|
||||
import {
|
||||
getModelTypeList,
|
||||
getModelList,
|
||||
createKnowledgeBase,
|
||||
updateKnowledgeBase,
|
||||
getKnowledgeGraphEntityTypes
|
||||
} from '@/api/knowledgeBase'
|
||||
import RbModal from '@/components/RbModal'
|
||||
const { TextArea } = Input;
|
||||
const { confirm } = Modal
|
||||
@@ -14,22 +20,104 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [modelTypeList, setModelTypeList] = useState<string[]>([]);
|
||||
const [modelOptionsByType, setModelOptionsByType] = useState<Record<string, { label: string; value: string }[]>>({});
|
||||
const [datasets, setDatasets] = useState<KnowledgeBaseListItem | null>(null);
|
||||
const [currentType, setCurrentType] = useState<'General' | 'Web' | 'Third-party' | 'Folder'>('General');
|
||||
const [form] = Form.useForm<KnowledgeBaseFormData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
const [generatingEntityTypes, setGeneratingEntityTypes] = useState(false);
|
||||
|
||||
// 监听 parser_config.graphrag 相关字段的变化
|
||||
const parserConfig = Form.useWatch('parser_config', form);
|
||||
const graphragConfig = parserConfig?.graphrag;
|
||||
const enableKnowledgeGraph = graphragConfig?.use_graphrag || false;
|
||||
const entityTypes = graphragConfig?.entity_types || '';
|
||||
const entityNormalization = graphragConfig?.resolution || false;
|
||||
const communityReportGeneration = graphragConfig?.community || false;
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setDatasets(null);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
setActiveTab('basic');
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
// 生成实体类型的函数
|
||||
const generateEntityTypes = async () => {
|
||||
const sceneName = form.getFieldValue(['parser_config', 'graphrag', 'scene_name']);
|
||||
if (!sceneName) {
|
||||
// 可以添加提示用户输入场景名称
|
||||
messageApi.error(t('knowledgeBase.enterScenarioName'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否选择了 LLM 模型
|
||||
const llmId = form.getFieldValue('llm_id');
|
||||
if (!llmId) {
|
||||
// 跳转到基础配置页
|
||||
setActiveTab('basic');
|
||||
messageApi.error(t('knowledgeBase.pleaseSelectLLMModel'));
|
||||
return;
|
||||
}
|
||||
|
||||
setGeneratingEntityTypes(true);
|
||||
try {
|
||||
// 这里应该调用实际的API接口
|
||||
// const user = JSON.parse(localStorage.getItem('user') as any);
|
||||
//datasets?.id || datasets?.parent_id || user?.current_workspace_id,
|
||||
const params = {
|
||||
scenario: sceneName,
|
||||
llm_id: llmId
|
||||
};
|
||||
const response = await getKnowledgeGraphEntityTypes(params);
|
||||
// 模拟API调用
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 处理API响应数据
|
||||
console.log('API Response:', response); // 调试日志
|
||||
|
||||
// 检查响应结构 - API直接返回字符串
|
||||
if (response && typeof response === 'string' && response.trim()) {
|
||||
// 将逗号分隔的字符串转换为换行分隔的格式以便在TextArea中显示
|
||||
const entityTypesString = response.replace(/,\s*/g, '\n');
|
||||
console.log('Converted entity types:', entityTypesString); // 调试日志
|
||||
|
||||
const currentGraphrag = form.getFieldValue(['parser_config', 'graphrag']) || {};
|
||||
const updatedGraphrag = {
|
||||
...currentGraphrag,
|
||||
entity_types: entityTypesString
|
||||
};
|
||||
|
||||
console.log('Updating form with:', updatedGraphrag); // 调试日志
|
||||
|
||||
// 使用更直接的方式更新表单字段
|
||||
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
|
||||
|
||||
// 强制触发表单重新渲染
|
||||
form.validateFields([['parser_config', 'graphrag', 'entity_types']]);
|
||||
|
||||
// 额外的强制更新机制
|
||||
setTimeout(() => {
|
||||
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
|
||||
}, 100);
|
||||
|
||||
messageApi.success(t('knowledgeBase.generateEntityTypesSuccess'));
|
||||
} else {
|
||||
messageApi.error(t('knowledgeBase.generateEntityTypesFailed') + ':' + t('knowledgeBase.unknownError'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('knowledgeBase.generateEntityTypesFailed') + ':', error);
|
||||
} finally {
|
||||
setGeneratingEntityTypes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const typeToFieldKey = (type: string): string => {
|
||||
switch ((type || '').toLowerCase()) {
|
||||
case 'embedding':
|
||||
@@ -89,6 +177,30 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
type: type || record.type || currentType,
|
||||
status: record.status,
|
||||
};
|
||||
|
||||
// 处理 parser_config 配置数据,如果没有则设置默认值
|
||||
baseValues.parser_config = record.parser_config || {
|
||||
graphrag: {
|
||||
use_graphrag: false,
|
||||
scene_name: '',
|
||||
entity_types: [] as any,
|
||||
method: 'general',
|
||||
resolution: false,
|
||||
community: false,
|
||||
}
|
||||
};
|
||||
|
||||
// 如果存在 entity_types,转换为换行分隔格式用于 TextArea 显示
|
||||
if (baseValues.parser_config.graphrag.entity_types) {
|
||||
if (Array.isArray(baseValues.parser_config.graphrag.entity_types)) {
|
||||
// 如果是数组格式,转换为换行分隔字符串
|
||||
(baseValues.parser_config.graphrag as any).entity_types = baseValues.parser_config.graphrag.entity_types.join('\n');
|
||||
} else if (typeof baseValues.parser_config.graphrag.entity_types === 'string') {
|
||||
// 如果是逗号分隔字符串格式,转换为换行分隔字符串(兼容旧数据)
|
||||
(baseValues.parser_config.graphrag as any).entity_types = (baseValues.parser_config.graphrag.entity_types as string).replace(/,\s*/g, '\n');
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldsValue(baseValues);
|
||||
};
|
||||
|
||||
@@ -142,12 +254,26 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
.then(() => {
|
||||
setLoading(true)
|
||||
const formValues = form.getFieldsValue();
|
||||
|
||||
// 处理 entity_types 格式转换:从换行分隔字符串转换为字符串数组
|
||||
if (formValues.parser_config && formValues.parser_config.graphrag && formValues.parser_config.graphrag.entity_types) {
|
||||
const entityTypesString = formValues.parser_config.graphrag.entity_types as any as string;
|
||||
const entityTypesArray = entityTypesString
|
||||
.split('\n')
|
||||
.map((item: string) => item.trim())
|
||||
.filter((item: string) => item.length > 0);
|
||||
formValues.parser_config.graphrag.entity_types = entityTypesArray;
|
||||
}
|
||||
|
||||
const payload: KnowledgeBaseFormData = {
|
||||
...formValues,
|
||||
type: formValues.type || currentType,
|
||||
permission_id: formValues.permission_id || 'Private',
|
||||
parent_id: datasets?.parent_id || undefined,
|
||||
};
|
||||
|
||||
console.log('Saving payload:', payload); // 调试日志
|
||||
|
||||
const submit = datasets?.id
|
||||
? updateKnowledgeBase(datasets.id, payload)
|
||||
: createKnowledgeBase(payload);
|
||||
@@ -205,6 +331,192 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
const dynamicTypeList = useMemo(() => modelTypeList.filter((tp) => (modelOptionsByType[tp] || []).length), [modelTypeList, modelOptionsByType]);
|
||||
|
||||
// 基础配置表单内容
|
||||
const renderBasicConfig = () => (
|
||||
<>
|
||||
{!datasets?.id && (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('knowledgeBase.createForm.name')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.createForm.name')} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="description" label={t('knowledgeBase.createForm.description')}>
|
||||
<TextArea rows={2} placeholder={t('knowledgeBase.createForm.description')} />
|
||||
</Form.Item>
|
||||
|
||||
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
// 当 tp 为 'llm' 时,合并 llm 和 chat 的选项
|
||||
const options = tp.toLowerCase() === 'llm'
|
||||
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
|
||||
: modelOptionsByType[tp] || [];
|
||||
return (
|
||||
<Form.Item
|
||||
key={tp}
|
||||
name={fieldKey as keyof KnowledgeBaseFormData}
|
||||
label={t(`knowledgeBase.createForm.${fieldKey}`) + ' ' + 'model'}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.modelRequired') }]}
|
||||
>
|
||||
<Select
|
||||
options={options}
|
||||
placeholder={t(`knowledgeBase.createForm.${fieldKey}`)}
|
||||
allowClear={false}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
onChange={(value) => handleChange(value, tp)}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
// 知识图谱配置表单内容
|
||||
const renderKnowledgeGraphConfig = () => (
|
||||
<>
|
||||
<div className={`rb:flex rb:w-full rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
enableKnowledgeGraph
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
: 'rb:border-[#EBEBEB]'
|
||||
}`}>
|
||||
<div className='rb:flex rb:flex-col rb:flex-1'>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium'>
|
||||
{t('knowledgeBase.enableKnowledgeGraph')}
|
||||
</div>
|
||||
<div className='rb:text-xs rb:text-[#5B6167] rb:mt-2'>
|
||||
{t('knowledgeBase.enableKnowledgeGraphTips')}
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'use_graphrag']}
|
||||
label=''
|
||||
valuePropName="checked"
|
||||
className='rb:mb-0'
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{enableKnowledgeGraph && (
|
||||
<>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium rb:mb-4'>
|
||||
{t('knowledgeBase.graphConfig')}
|
||||
</div>
|
||||
{/* 场景名称 */}
|
||||
<div className='rb:flex rb:items-center rb:gap-2'>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'scene_name']}
|
||||
label={t('knowledgeBase.sceneName')}
|
||||
className='rb:w-full rb:min-w-[240px]'
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') + t('knowledgeBase.sceneName') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.sceneNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={generatingEntityTypes}
|
||||
onClick={generateEntityTypes}
|
||||
className='rb:mt-1'
|
||||
>
|
||||
{!(entityTypes as any as string) || (entityTypes as any as string).trim() === ''
|
||||
? t('knowledgeBase.generateEntityTypes')
|
||||
: t('knowledgeBase.regenerateEntityTypes')
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 实体类型 */}
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'entity_types']}
|
||||
label={t('knowledgeBase.entityTypes')}
|
||||
>
|
||||
<TextArea
|
||||
rows={4}
|
||||
placeholder={t('knowledgeBase.entityTypesPlaceholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 实体归一化 */}
|
||||
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
entityNormalization
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
: 'rb:border-[#EBEBEB]'
|
||||
}`}>
|
||||
<div className='rb:flex rb:flex-col rb:flex-1'>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium'>
|
||||
{t('knowledgeBase.entityNormalization')}
|
||||
</div>
|
||||
<div className='rb:text-xs rb:text-[#5B6167] rb:mt-2'>
|
||||
{t('knowledgeBase.entityNormalizationTips')}
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'resolution']}
|
||||
valuePropName="checked"
|
||||
className='rb:mb-0'
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 实体方法 */}
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'method']}
|
||||
label={t('knowledgeBase.entityMethod')}
|
||||
initialValue="general"
|
||||
>
|
||||
<Radio.Group>
|
||||
<Radio value="general">{t('knowledgeBase.entityMethodGeneral')}</Radio>
|
||||
<Radio value="light">{t('knowledgeBase.entityMethodLight')}</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 社区报告生成 */}
|
||||
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
communityReportGeneration
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
: 'rb:border-[#EBEBEB]'
|
||||
}`}>
|
||||
<div className='rb:flex rb:flex-col rb:flex-1'>
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium'>
|
||||
{t('knowledgeBase.communityReportGeneration')}
|
||||
</div>
|
||||
<div className='rb:text-xs rb:text-[#5B6167] rb:mt-2'>
|
||||
{t('knowledgeBase.communityReportGenerationTips')}
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'community']}
|
||||
valuePropName="checked"
|
||||
className='rb:mb-0'
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Tabs 配置
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'basic',
|
||||
label: t('knowledgeBase.basicConfig'),
|
||||
children: renderBasicConfig(),
|
||||
},
|
||||
{
|
||||
key: 'knowledgeGraph',
|
||||
label: t('knowledgeBase.knowledgeGraph'),
|
||||
children: renderKnowledgeGraphConfig(),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={getTitle()}
|
||||
@@ -220,48 +532,25 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
initialValues={{
|
||||
permission_id: 'Private', // 设置 permission_id 的默认值
|
||||
type: currentType,
|
||||
parser_config: {
|
||||
graphrag: {
|
||||
use_graphrag: false, // 默认不启用知识图谱
|
||||
scene_name: '', // 场景名称
|
||||
entity_types: '' as any, // 实体类型(界面上显示为字符串,保存时转为数组)
|
||||
method: 'general', // 默认使用通用方法
|
||||
resolution: false, // 默认不启用实体归一化
|
||||
community: false, // 默认不生成社区报告
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* <div className="rb:text-[14px] rb:font-medium rb:text-[#5B6167] rb:mb-[16px]">{t('model.basicParameters')}</div> */}
|
||||
{!datasets?.id && (
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('knowledgeBase.createForm.name')}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.nameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('knowledgeBase.createForm.name')} />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="description" label={t('knowledgeBase.createForm.description')}>
|
||||
<TextArea rows={2} placeholder={t('knowledgeBase.createForm.description')} />
|
||||
</Form.Item>
|
||||
|
||||
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
// 当 tp 为 'llm' 时,合并 llm 和 chat 的选项
|
||||
const options = tp.toLowerCase() === 'llm'
|
||||
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
|
||||
: modelOptionsByType[tp] || [];
|
||||
return (
|
||||
<Form.Item
|
||||
key={tp}
|
||||
name={fieldKey as keyof KnowledgeBaseFormData}
|
||||
label={t(`knowledgeBase.createForm.${fieldKey}`) + ' ' + 'model'}
|
||||
rules={[{ required: true, message: t('knowledgeBase.createForm.modelRequired') }]}
|
||||
>
|
||||
<Select
|
||||
options={options}
|
||||
placeholder={t(`knowledgeBase.createForm.${fieldKey}`)}
|
||||
allowClear={false}
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
onChange={(value) => handleChange(value, tp)}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
})}
|
||||
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
/>
|
||||
</Form>
|
||||
{contextHolder}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
451
web/src/views/KnowledgeBase/components/KnowledgeGraph.tsx
Normal file
451
web/src/views/KnowledgeBase/components/KnowledgeGraph.tsx
Normal file
@@ -0,0 +1,451 @@
|
||||
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Col } from 'antd'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import ReactEcharts from 'echarts-for-react'
|
||||
import zoom from '@/assets/images/userMemory/zoom.svg'
|
||||
import drag from '@/assets/images/userMemory/drag.svg'
|
||||
import pointer from '@/assets/images/userMemory/pointer.svg'
|
||||
import empty from '@/assets/images/userMemory/empty.svg'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
// 知识图谱数据类型定义
|
||||
export interface KnowledgeNode {
|
||||
id: string
|
||||
entity_name: string
|
||||
entity_type: string
|
||||
description: string
|
||||
pagerank: number
|
||||
source_id: string[]
|
||||
// ECharts 需要的属性
|
||||
name: string
|
||||
category: number
|
||||
symbolSize: number
|
||||
itemStyle: {
|
||||
color: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface KnowledgeEdge {
|
||||
src_id: string
|
||||
tgt_id: string
|
||||
description: string
|
||||
keywords: string[]
|
||||
weight: number
|
||||
source_id: string[]
|
||||
source: string
|
||||
target: string
|
||||
// ECharts 需要的属性
|
||||
value: number
|
||||
}
|
||||
|
||||
export interface KnowledgeGraphData {
|
||||
directed: boolean
|
||||
multigraph: boolean
|
||||
graph: {
|
||||
source_id: string[]
|
||||
}
|
||||
nodes: KnowledgeNode[]
|
||||
edges: KnowledgeEdge[]
|
||||
}
|
||||
|
||||
export interface KnowledgeGraphResponse {
|
||||
graph: KnowledgeGraphData
|
||||
mind_map: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface KnowledgeGraphProps {
|
||||
data?: KnowledgeGraphResponse
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const operations = [
|
||||
{ name: 'click', icon: pointer },
|
||||
{ name: 'drag', icon: drag },
|
||||
{ name: 'zoom', icon: zoom },
|
||||
]
|
||||
|
||||
// 预定义的颜色调色板
|
||||
const colorPalette = [
|
||||
'#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21',
|
||||
'#FF5D34', '#FF8A4C', '#FFB048', '#E74C3C', '#9B59B6',
|
||||
'#3498DB', '#1ABC9C', '#F39C12', '#D35400', '#C0392B',
|
||||
'#8E44AD', '#2980B9', '#16A085', '#F1C40F', '#E67E22'
|
||||
]
|
||||
|
||||
// 动态生成实体类型颜色映射
|
||||
const generateEntityTypeColors = (entityTypes: string[]): Record<string, string> => {
|
||||
const colorMap: Record<string, string> = {}
|
||||
entityTypes.forEach((type, index) => {
|
||||
colorMap[type] = colorPalette[index % colorPalette.length]
|
||||
})
|
||||
return colorMap
|
||||
}
|
||||
|
||||
const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
const { t } = useTranslation()
|
||||
const chartRef = useRef<ReactEcharts>(null)
|
||||
const resizeScheduledRef = useRef(false)
|
||||
const modalRef = useRef<HTMLDivElement>(null)
|
||||
const [nodes, setNodes] = useState<KnowledgeNode[]>([])
|
||||
const [links, setLinks] = useState<KnowledgeEdge[]>([])
|
||||
const [categories, setCategories] = useState<{ name: string }[]>([])
|
||||
const [selectedNode, setSelectedNode] = useState<KnowledgeNode | null>(null)
|
||||
const [entityTypeColors, setEntityTypeColors] = useState<Record<string, string>>({})
|
||||
|
||||
// 弹框拖动相关状态
|
||||
const [modalPosition, setModalPosition] = useState({ x: 20, y: 20 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
|
||||
// 拖动处理函数
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
setIsDragging(true)
|
||||
setDragStart({
|
||||
x: e.clientX - modalPosition.x,
|
||||
y: e.clientY - modalPosition.y
|
||||
})
|
||||
}, [modalPosition])
|
||||
|
||||
const handleMouseMove = useCallback((e: MouseEvent) => {
|
||||
if (!isDragging) return
|
||||
|
||||
const newX = e.clientX - dragStart.x
|
||||
const newY = e.clientY - dragStart.y
|
||||
|
||||
// 限制拖动范围,确保弹框不会超出容器
|
||||
const container = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (container && modalRef.current) {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const modalRect = modalRef.current.getBoundingClientRect()
|
||||
|
||||
const maxX = containerRect.width - modalRect.width
|
||||
const maxY = containerRect.height - modalRect.height
|
||||
|
||||
setModalPosition({
|
||||
x: Math.max(0, Math.min(newX, maxX)),
|
||||
y: Math.max(0, Math.min(newY, maxY))
|
||||
})
|
||||
}
|
||||
}, [isDragging, dragStart])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
// 添加全局鼠标事件监听
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
document.addEventListener('mouseup', handleMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
document.removeEventListener('mouseup', handleMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging, handleMouseMove, handleMouseUp])
|
||||
|
||||
// 关闭弹框
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setSelectedNode(null)
|
||||
}, [])
|
||||
|
||||
// 处理知识图谱数据
|
||||
const processGraphData = useCallback(() => {
|
||||
if (!data?.graph) {
|
||||
setNodes([])
|
||||
setLinks([])
|
||||
setCategories([])
|
||||
setSelectedNode(null)
|
||||
return
|
||||
}
|
||||
|
||||
const { nodes: rawNodes, edges: rawEdges } = data.graph
|
||||
const processedNodes: KnowledgeNode[] = []
|
||||
const processedEdges: KnowledgeEdge[] = []
|
||||
|
||||
// 获取所有实体类型
|
||||
const entityTypes = [...new Set(rawNodes.map(node => node.entity_type))]
|
||||
const categoryMap = entityTypes.reduce((acc, type, index) => {
|
||||
acc[type] = index
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
// 动态生成实体类型颜色映射
|
||||
const dynamicEntityTypeColors = generateEntityTypeColors(entityTypes)
|
||||
setEntityTypeColors(dynamicEntityTypeColors)
|
||||
|
||||
// 计算每个节点的连接数
|
||||
const connectionCount: Record<string, number> = {}
|
||||
rawEdges.forEach(edge => {
|
||||
// 使用 src_id 和 tgt_id 计算连接数
|
||||
connectionCount[edge.src_id] = (connectionCount[edge.src_id] || 0) + 1
|
||||
connectionCount[edge.tgt_id] = (connectionCount[edge.tgt_id] || 0) + 1
|
||||
})
|
||||
|
||||
// 处理节点数据
|
||||
rawNodes.forEach(node => {
|
||||
const connections = connectionCount[node.id] || 0
|
||||
const categoryIndex = categoryMap[node.entity_type] || 0
|
||||
|
||||
// 根据 pagerank 和连接数计算节点大小
|
||||
let symbolSize = Math.max(10, Math.min(50, node.pagerank * 200 + connections * 2))
|
||||
|
||||
processedNodes.push({
|
||||
...node,
|
||||
name: node.entity_name,
|
||||
category: categoryIndex,
|
||||
symbolSize,
|
||||
itemStyle: {
|
||||
color: dynamicEntityTypeColors[node.entity_type] || colorPalette[0]
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 处理边数据
|
||||
rawEdges.forEach(edge => {
|
||||
// 注意:根据数据结构,source 和 target 字段可能与 src_id 和 tgt_id 相反
|
||||
// 我们使用 src_id 和 tgt_id 作为正确的连接关系
|
||||
processedEdges.push({
|
||||
...edge, // 保留所有原始字段
|
||||
source: edge.src_id, // 使用 src_id 作为源节点
|
||||
target: edge.tgt_id, // 使用 tgt_id 作为目标节点
|
||||
value: edge.weight || 1
|
||||
})
|
||||
})
|
||||
|
||||
// 验证节点ID和边的连接
|
||||
const nodeIds = new Set(processedNodes.map(n => n.id))
|
||||
const validEdges = processedEdges.filter(edge => {
|
||||
const sourceExists = nodeIds.has(edge.source)
|
||||
const targetExists = nodeIds.has(edge.target)
|
||||
if (!sourceExists || !targetExists) {
|
||||
console.warn('Invalid edge:', edge, 'Source exists:', sourceExists, 'Target exists:', targetExists)
|
||||
}
|
||||
return sourceExists && targetExists
|
||||
})
|
||||
|
||||
// 调试信息
|
||||
console.log('Total nodes:', processedNodes.length)
|
||||
console.log('Total edges:', processedEdges.length)
|
||||
console.log('Valid edges:', validEdges.length)
|
||||
console.log('Node IDs:', Array.from(nodeIds).slice(0, 5))
|
||||
console.log('Edge sample:', validEdges.slice(0, 3))
|
||||
|
||||
// 设置分类
|
||||
const processedCategories = entityTypes.map(type => ({ name: type }))
|
||||
|
||||
setNodes(processedNodes)
|
||||
setLinks(validEdges) // 只使用有效的边
|
||||
setCategories(processedCategories)
|
||||
}, [data])
|
||||
|
||||
useEffect(() => {
|
||||
processGraphData()
|
||||
}, [processGraphData])
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (chartRef.current && !resizeScheduledRef.current) {
|
||||
resizeScheduledRef.current = true
|
||||
requestAnimationFrame(() => {
|
||||
chartRef.current?.getEchartsInstance().resize()
|
||||
resizeScheduledRef.current = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(handleResize)
|
||||
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (chartElement) {
|
||||
resizeObserver.observe(chartElement)
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [nodes])
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<RbCard
|
||||
title={t('knowledgeBase.knowledgeGraph')}
|
||||
headerType="borderless"
|
||||
headerClassName="rb:text-[18px]! rb:leading-[24px]"
|
||||
>
|
||||
<div className="rb:h-124 rb:relative">
|
||||
{loading ? (
|
||||
<div className="rb:h-full rb:flex rb:items-center rb:justify-center">
|
||||
<div className="rb:text-[#5B6167]">加载中...</div>
|
||||
</div>
|
||||
) : nodes.length === 0 ? (
|
||||
<Empty className="rb:h-full" />
|
||||
) : (
|
||||
<>
|
||||
<ReactEcharts
|
||||
ref={chartRef}
|
||||
option={{
|
||||
colors: Object.values(entityTypeColors),
|
||||
tooltip: {
|
||||
show: true,
|
||||
formatter: (params: any) => {
|
||||
if (params.dataType === 'node') {
|
||||
const node = params.data as KnowledgeNode
|
||||
return `
|
||||
<div>
|
||||
<div><strong>${node.entity_name}</strong></div>
|
||||
<div>类型: ${node.entity_type}</div>
|
||||
<div>重要度: ${(node.pagerank * 100).toFixed(2)}%</div>
|
||||
</div>
|
||||
`
|
||||
} else if (params.dataType === 'edge') {
|
||||
const edge = params.data as KnowledgeEdge
|
||||
return `
|
||||
<div>
|
||||
<div><strong>关系</strong></div>
|
||||
<div>权重: ${edge.weight}</div>
|
||||
<div>${edge.description}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
data: categories.map(cat => cat.name),
|
||||
orient: 'vertical',
|
||||
left: 'right',
|
||||
top: 'center'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
data: nodes,
|
||||
links: links,
|
||||
categories: categories,
|
||||
roam: true,
|
||||
label: {
|
||||
show: true,
|
||||
position: 'right',
|
||||
formatter: '{b}',
|
||||
fontSize: 12
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#5B6167',
|
||||
curveness: 0.3,
|
||||
width: 2, // 固定线宽,避免函数问题
|
||||
opacity: 0.8
|
||||
},
|
||||
force: {
|
||||
repulsion: 300,
|
||||
edgeLength: 150,
|
||||
gravity: 0.1,
|
||||
layoutAnimation: true,
|
||||
preventOverlap: true
|
||||
},
|
||||
selectedMode: 'single',
|
||||
draggable: true,
|
||||
animationDurationUpdate: 0,
|
||||
select: {
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff',
|
||||
shadowBlur: 10,
|
||||
}
|
||||
},
|
||||
emphasis: {
|
||||
focus: 'adjacency',
|
||||
lineStyle: {
|
||||
width: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
style={{ height: '496px', width: '100%' }}
|
||||
notMerge={false}
|
||||
lazyUpdate={true}
|
||||
onEvents={{
|
||||
click: (params: { dataType: string; data: KnowledgeNode }) => {
|
||||
if (params.dataType === 'node') {
|
||||
console.log('Knowledge node clicked:', params.data)
|
||||
setSelectedNode(params.data)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 实体详情弹框 */}
|
||||
{selectedNode && (
|
||||
<div
|
||||
ref={modalRef}
|
||||
className="rb:absolute rb:bg-white rb:border rb:border-[#EBEBEB] rb:rounded-[12px] rb:shadow-lg rb:p-4 rb:w-80 rb:z-10"
|
||||
style={{
|
||||
left: modalPosition.x,
|
||||
top: modalPosition.y,
|
||||
cursor: isDragging ? 'grabbing' : 'grab'
|
||||
}}
|
||||
>
|
||||
{/* 弹框头部 - 可拖动区域 */}
|
||||
<div
|
||||
className="rb:flex rb:items-center rb:justify-between rb:mb-3 rb:pb-2 rb:border-b rb:border-[#EBEBEB] rb:cursor-grab"
|
||||
onMouseDown={handleMouseDown}
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:text-[#1A1A1A]">
|
||||
{t('knowledgeBase.entityDetails')}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCloseModal}
|
||||
className="rb:w-6 rb:h-6 rb:flex rb:items-center rb:justify-center rb:text-[#5B6167] hover:rb:text-[#1A1A1A] hover:rb:bg-[#F0F3F8] rb:rounded rb:transition-colors"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 弹框内容 */}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:mb-4">
|
||||
<div className="rb:text-[16px] rb:mb-2">{selectedNode.entity_name}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-2">
|
||||
<span className="rb:inline-block rb:px-2 rb:py-1 rb:bg-[#F0F3F8] rb:rounded rb:mr-2">
|
||||
{selectedNode.entity_type}
|
||||
</span>
|
||||
<span>重要度: {(selectedNode.pagerank * 100).toFixed(2)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:font-medium rb:mb-4">
|
||||
{t('knowledgeBase.entityDescription')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:leading-5">
|
||||
{selectedNode.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:font-medium rb:mb-2">
|
||||
{t('knowledgeBase.sourceDocuments')}
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
|
||||
{selectedNode.source_id.length} 个文档
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-6 rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
|
||||
{operations.map((item) => (
|
||||
<div key={item.name} className="rb:flex rb:items-center rb:text-[#5B6167] rb:leading-5">
|
||||
<img src={item.icon} className="rb:w-5 rb:h-5 rb:mr-1" />
|
||||
{t(`userMemory.${item.name}`)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</RbCard>
|
||||
</Col>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(KnowledgeGraph)
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* @Description:
|
||||
* @Version: 0.0.1
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-12-30 15:07:37
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2026-01-04 20:15:12
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Row } from 'antd'
|
||||
import KnowledgeGraph, { type KnowledgeGraphResponse } from './KnowledgeGraph'
|
||||
import { getKnowledgeGraph } from '@/api/knowledgeBase';
|
||||
|
||||
interface KnowledgeGraphCardProps {
|
||||
knowledgeBaseId?: string;
|
||||
}
|
||||
|
||||
const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBaseId }) => {
|
||||
const { t } = useTranslation();
|
||||
const [data, setData] = useState<KnowledgeGraphResponse | undefined>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!knowledgeBaseId) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getKnowledgeGraph(knowledgeBaseId)
|
||||
setData(res as KnowledgeGraphResponse)
|
||||
} catch (error) {
|
||||
console.error('获取知识图谱数据失败:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchData()
|
||||
}, [knowledgeBaseId])
|
||||
|
||||
return (
|
||||
<div className='rb:flex rb:flex-col'>
|
||||
<div className='rb:flex rb:flex-col rb:p-4'>
|
||||
<div className='rb:w-full rb:text-lg rb:font-medium rb:text-[#212332] rb:leading-6'>
|
||||
{t('knowledgeBase.graphTitle')}
|
||||
</div>
|
||||
<div className='rb:w-full rb:text-xs rb:text-[#5B6167] rb:leading-4 rb:mt-2'>
|
||||
{t('knowledgeBase.graphTips')}
|
||||
</div>
|
||||
<div className='rb:flex rb:items-center rb:justify-between'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='rb:p-4'>
|
||||
<KnowledgeGraph data={data} loading={loading} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default KnowledgeGraphCard
|
||||
163
web/src/views/KnowledgeBase/components/README.md
Normal file
163
web/src/views/KnowledgeBase/components/README.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# KnowledgeGraph 组件
|
||||
|
||||
基于 ECharts 的知识图谱可视化组件,用于展示知识库中实体之间的关系网络。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🎯 **交互式图谱**: 支持节点点击、拖拽、缩放等交互操作
|
||||
- 🎨 **实体分类**: 根据实体类型自动分配颜色和图例
|
||||
- 📊 **智能布局**: 基于力导向算法的自动布局
|
||||
- 🔍 **详情展示**: 点击节点查看实体详细信息
|
||||
- 📱 **响应式设计**: 自适应容器大小变化
|
||||
- 🌐 **国际化支持**: 支持中英文切换
|
||||
|
||||
## 数据结构
|
||||
|
||||
### KnowledgeGraphResponse
|
||||
```typescript
|
||||
interface KnowledgeGraphResponse {
|
||||
code: number
|
||||
msg: string
|
||||
data: {
|
||||
graph: KnowledgeGraphData
|
||||
mind_map: Record<string, unknown>
|
||||
}
|
||||
error: string
|
||||
time: number
|
||||
}
|
||||
```
|
||||
|
||||
### KnowledgeNode
|
||||
```typescript
|
||||
interface KnowledgeNode {
|
||||
id: string // 节点唯一标识
|
||||
entity_name: string // 实体名称
|
||||
entity_type: string // 实体类型 (ORGANIZATION, PERSON, EVENT, CATEGORY, etc.)
|
||||
description: string // 实体描述
|
||||
pagerank: number // PageRank 重要度分数
|
||||
source_id: string[] // 来源文档ID列表
|
||||
}
|
||||
```
|
||||
|
||||
### KnowledgeEdge
|
||||
```typescript
|
||||
interface KnowledgeEdge {
|
||||
src_id: string // 源节点ID
|
||||
tgt_id: string // 目标节点ID
|
||||
description: string // 关系描述
|
||||
keywords: string[] // 关键词
|
||||
weight: number // 关系权重
|
||||
source_id: string[] // 来源文档ID列表
|
||||
source: string // 源节点名称
|
||||
target: string // 目标节点名称
|
||||
}
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基础用法
|
||||
|
||||
```tsx
|
||||
import KnowledgeGraph from './components/KnowledgeGraph'
|
||||
|
||||
const MyComponent = () => {
|
||||
const [graphData, setGraphData] = useState<KnowledgeGraphResponse>()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
return (
|
||||
<KnowledgeGraph
|
||||
data={graphData}
|
||||
loading={loading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```tsx
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Row } from 'antd'
|
||||
import KnowledgeGraph from './components/KnowledgeGraph'
|
||||
|
||||
const KnowledgeBasePage = () => {
|
||||
const [data, setData] = useState()
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchKnowledgeGraph = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await api.getKnowledgeGraph(knowledgeBaseId)
|
||||
setData(response)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch knowledge graph:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchKnowledgeGraph()
|
||||
}, [knowledgeBaseId])
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<KnowledgeGraph data={data} loading={loading} />
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 组件属性
|
||||
|
||||
| 属性 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| data | KnowledgeGraphResponse | undefined | 知识图谱数据 |
|
||||
| loading | boolean | false | 加载状态 |
|
||||
|
||||
## 实体类型颜色
|
||||
|
||||
组件内置了以下实体类型的颜色映射:
|
||||
|
||||
- `ORGANIZATION`: #155EEF (蓝色)
|
||||
- `PERSON`: #4DA8FF (浅蓝色)
|
||||
- `EVENT`: #9C6FFF (紫色)
|
||||
- `CATEGORY`: #8BAEF7 (淡蓝色)
|
||||
- `LOCATION`: #369F21 (绿色)
|
||||
- `TIME`: #FF5D34 (橙红色)
|
||||
- `CONCEPT`: #FF8A4C (橙色)
|
||||
- `OTHER`: #FFB048 (黄色)
|
||||
|
||||
## 交互功能
|
||||
|
||||
1. **节点点击**: 点击节点查看实体详细信息
|
||||
2. **拖拽**: 拖拽节点调整位置
|
||||
3. **缩放**: 鼠标滚轮缩放图谱
|
||||
4. **悬停**: 悬停显示节点和边的详细信息
|
||||
5. **高亮**: 点击节点高亮相邻节点和边
|
||||
|
||||
## 国际化
|
||||
|
||||
组件使用以下翻译键:
|
||||
|
||||
- `knowledgeBase.knowledgeGraph`: 知识图谱标题
|
||||
- `knowledgeBase.entityDetails`: 实体详情标题
|
||||
- `knowledgeBase.entityDetailEmpty`: 空状态提示
|
||||
- `knowledgeBase.entityDetailEmptyDesc`: 空状态描述
|
||||
- `knowledgeBase.entityDescription`: 实体描述标签
|
||||
- `knowledgeBase.sourceDocuments`: 来源文档标签
|
||||
- `userMemory.click/drag/zoom`: 操作说明
|
||||
|
||||
## 性能优化
|
||||
|
||||
- 使用 `React.memo` 避免不必要的重渲染
|
||||
- 使用 `ResizeObserver` 监听容器大小变化
|
||||
- 使用 `requestAnimationFrame` 优化图表重绘
|
||||
- 延迟更新和懒加载提升大数据集性能
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 确保传入的数据结构符合 `KnowledgeGraphResponse` 接口
|
||||
2. 节点的 `pagerank` 值用于计算节点大小,建议范围在 0-1 之间
|
||||
3. 边的 `weight` 值用于计算连线粗细,建议使用正数
|
||||
4. 大数据集可能影响渲染性能,建议进行数据分页或过滤
|
||||
@@ -14,6 +14,15 @@ export interface KnowledgeBaseFormData {
|
||||
parent_id?: string; // 父ID
|
||||
type?: string; // 知识库类型
|
||||
status?: number; // 状态
|
||||
parser_config: ParserConfig; // 解析器配置
|
||||
}
|
||||
export interface GraphragConfig{
|
||||
use_graphrag:boolean; // 是否启用图谱
|
||||
scene_name: string; // 场景名称
|
||||
entity_types: Array<string>; // 实体类型
|
||||
method: string; // 方法
|
||||
resolution: boolean; // 实体归一化
|
||||
community: boolean; /// 是否生成社区报告
|
||||
}
|
||||
export interface KnowledgeBase {
|
||||
id: string;
|
||||
@@ -86,6 +95,8 @@ export interface ParserConfig {
|
||||
auto_keywords?: number; // 自动关键词
|
||||
auto_questions?: number; // 自动问题
|
||||
html4excel?: boolean; // 是否为Excel文件
|
||||
graphrag:GraphragConfig; // 知识图谱生成
|
||||
|
||||
}
|
||||
// 文件数据
|
||||
export interface KnowledgeBaseDocumentData { // 知识库文档数据
|
||||
|
||||
Reference in New Issue
Block a user