feat(web): add Ontology menu

This commit is contained in:
zhaoying
2026-01-29 17:13:19 +08:00
parent cca3900678
commit d1b4f2b6c2
16 changed files with 916 additions and 24 deletions

39
web/src/api/ontology.ts Normal file
View File

@@ -0,0 +1,39 @@
import { request } from '@/utils/request'
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData } from '@/views/Ontology/types'
// Scene list
export const getOntologyScenesUrl = '/memory/ontology/scenes'
export const getOntologyScenesList = (data: Query) => {
return request.get(getOntologyScenesUrl, data)
}
// Create scene
export const createOntologyScene = (data: OntologyModalData) => {
return request.post('/memory/ontology/scene', data)
}
// Update scene
export const updateOntologyScene = (scene_id: string, data: OntologyModalData) => {
return request.put(`/memory/ontology/scene/${scene_id}`, data)
}
// Delete scene
export const deleteOntologyScene = (scene_id: string) => {
return request.delete(`/memory/ontology/scene/${scene_id}`)
}
// Get class list
export const getOntologyclassesUrl = '/memory/ontology/classes'
export const getOntologyClassList = (data: { scene_id: string; class_name?: string; }) => {
return request.get(getOntologyclassesUrl, data)
}
// Extract ontology types
export const extractOntologyTypes = (data: OntologyClassExtractModalData) => {
return request.post('/memory/ontology/extract', data)
}
// Create ontology class
export const createOntologyClass = (data: OntologyClassModalData) => {
return request.post('/memory/ontology/class', data)
}
// Delete ontology class
export const deleteOntologyClass = (class_id: string) => {
return request.delete(`/memory/ontology/class/${class_id}`)
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>本体管理备份</title>
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-记忆管理" transform="translate(-54, -600)" fill="#5B6167" fill-rule="nonzero">
<g id="本体管理备份" transform="translate(54, 600)">
<path d="M12.9051096,10.4106225 C12.3694196,11.4980012 11.4899287,12.3774986 10.4105534,12.9131925 C10.2426506,14.0965163 9.22723825,15 8.00394627,15 C6.77265892,15 5.75724661,14.0885208 5.59733917,12.905197 C4.50996851,12.3695032 3.63047754,11.4900057 3.09478759,10.4106225 C1.91147246,10.2427185 1,9.22729869 1,8.00399772 C1,6.77270132 1.91147246,5.76527699 3.09478759,5.59737293 C3.63047754,4.50999429 4.50996851,3.63049686 5.59733917,3.09480297 C5.76524199,1.91147915 6.77265892,1 8.00394627,1 C9.22723826,1 10.2426506,1.90348372 10.4105534,3.08680754 C11.4899287,3.62250143 12.3694196,4.50199886 12.9051096,5.5893775 C14.0884247,5.75728156 14.9999489,6.76470589 14.9999489,7.99600228 C15.0078925,9.23529411 14.0884247,10.2507139 12.9051096,10.4106225 Z M8.00394627,13.7846945 C8.67555756,13.7846945 9.21924289,13.2410052 9.21924289,12.5693889 C9.21924289,11.8977727 8.67555756,11.3540834 8.00394627,11.3540834 C7.33233498,11.3540834 6.78864966,11.8977727 6.78864966,12.5693889 C6.78864966,13.2410052 7.33233498,13.7846945 8.00394627,13.7846945 Z M3.43858861,6.78069676 C2.76697732,6.78069676 2.22329199,7.32438608 2.22329199,7.99600228 C2.22329199,8.66761849 2.76697732,9.21130783 3.43858861,9.21130783 C4.11019989,9.21130783 4.65388521,8.67561394 4.65388521,8.00399772 C4.65388521,7.3323815 4.11019988,6.78069676 3.43858861,6.78069676 Z M8.00394627,2.21530554 C7.33233498,2.21530554 6.78864966,2.75899486 6.78864966,3.43860652 C6.78864966,4.11821817 7.33233498,4.65391206 8.00394627,4.65391206 C8.67555756,4.65391206 9.21924289,4.11022274 9.21924289,3.43860652 C9.21924289,2.7669903 8.67555756,2.21530554 8.00394627,2.21530554 L8.00394627,2.21530554 Z M10.2506459,4.38206739 C9.8828588,5.25356939 9.0113632,5.86921759 8.00394627,5.86921759 C6.99652934,5.86921759 6.12503374,5.25356939 5.75724661,4.38206739 C5.19757054,4.72587094 4.72584357,5.19760137 4.38204255,5.75728156 C5.25353815,6.12507139 5.86918182,6.98857795 5.86918182,8.00399772 C5.86918182,9.01142205 5.25353815,9.87492861 4.38204255,10.2507139 C4.72584357,10.8103941 5.19757054,11.2821245 5.75724661,11.625928 C6.12503374,10.754426 6.98853397,10.1387778 8.00394627,10.1387778 C9.0113632,10.1387778 9.87486343,10.754426 10.2506459,11.625928 C10.8023266,11.2821245 11.2740536,10.8103941 11.62585,10.2507139 C10.7543544,9.88292405 10.1387107,9.01941748 10.1387107,8.01199315 C10.1387107,7.00456882 10.7543544,6.14106226 11.62585,5.76527699 C11.2740536,5.19760138 10.8023266,4.73386637 10.2506459,4.38206739 Z M12.569304,6.78069676 C11.8976927,6.78069676 11.3540073,7.32438608 11.3540073,7.99600228 C11.3540073,8.66761849 11.8976927,9.21130783 12.569304,9.21130783 C13.2409152,9.21130783 13.7846006,8.6676185 13.7846006,7.99600228 C13.7846006,7.32438606 13.2409152,6.78069676 12.569304,6.78069676 L12.569304,6.78069676 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>本体管理</title>
<g id="v0.2.0" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="红熊空间-记忆管理" transform="translate(-28, -600)" fill="#212332" fill-rule="nonzero">
<g id="本体管理" transform="translate(28, 600)">
<path d="M12.9051096,10.4106225 C12.3694196,11.4980012 11.4899287,12.3774986 10.4105534,12.9131925 C10.2426506,14.0965163 9.22723825,15 8.00394627,15 C6.77265892,15 5.75724661,14.0885208 5.59733917,12.905197 C4.50996851,12.3695032 3.63047754,11.4900057 3.09478759,10.4106225 C1.91147246,10.2427185 1,9.22729869 1,8.00399772 C1,6.77270132 1.91147246,5.76527699 3.09478759,5.59737293 C3.63047754,4.50999429 4.50996851,3.63049686 5.59733917,3.09480297 C5.76524199,1.91147915 6.77265892,1 8.00394627,1 C9.22723826,1 10.2426506,1.90348372 10.4105534,3.08680754 C11.4899287,3.62250143 12.3694196,4.50199886 12.9051096,5.5893775 C14.0884247,5.75728156 14.9999489,6.76470589 14.9999489,7.99600228 C15.0078925,9.23529411 14.0884247,10.2507139 12.9051096,10.4106225 Z M8.00394627,13.7846945 C8.67555756,13.7846945 9.21924289,13.2410052 9.21924289,12.5693889 C9.21924289,11.8977727 8.67555756,11.3540834 8.00394627,11.3540834 C7.33233498,11.3540834 6.78864966,11.8977727 6.78864966,12.5693889 C6.78864966,13.2410052 7.33233498,13.7846945 8.00394627,13.7846945 Z M3.43858861,6.78069676 C2.76697732,6.78069676 2.22329199,7.32438608 2.22329199,7.99600228 C2.22329199,8.66761849 2.76697732,9.21130783 3.43858861,9.21130783 C4.11019989,9.21130783 4.65388521,8.67561394 4.65388521,8.00399772 C4.65388521,7.3323815 4.11019988,6.78069676 3.43858861,6.78069676 Z M8.00394627,2.21530554 C7.33233498,2.21530554 6.78864966,2.75899486 6.78864966,3.43860652 C6.78864966,4.11821817 7.33233498,4.65391206 8.00394627,4.65391206 C8.67555756,4.65391206 9.21924289,4.11022274 9.21924289,3.43860652 C9.21924289,2.7669903 8.67555756,2.21530554 8.00394627,2.21530554 L8.00394627,2.21530554 Z M10.2506459,4.38206739 C9.8828588,5.25356939 9.0113632,5.86921759 8.00394627,5.86921759 C6.99652934,5.86921759 6.12503374,5.25356939 5.75724661,4.38206739 C5.19757054,4.72587094 4.72584357,5.19760137 4.38204255,5.75728156 C5.25353815,6.12507139 5.86918182,6.98857795 5.86918182,8.00399772 C5.86918182,9.01142205 5.25353815,9.87492861 4.38204255,10.2507139 C4.72584357,10.8103941 5.19757054,11.2821245 5.75724661,11.625928 C6.12503374,10.754426 6.98853397,10.1387778 8.00394627,10.1387778 C9.0113632,10.1387778 9.87486343,10.754426 10.2506459,11.625928 C10.8023266,11.2821245 11.2740536,10.8103941 11.62585,10.2507139 C10.7543544,9.88292405 10.1387107,9.01941748 10.1387107,8.01199315 C10.1387107,7.00456882 10.7543544,6.14106226 11.62585,5.76527699 C11.2740536,5.19760138 10.8023266,4.73386637 10.2506459,4.38206739 Z M12.569304,6.78069676 C11.8976927,6.78069676 11.3540073,7.32438608 11.3540073,7.99600228 C11.3540073,8.66761849 11.8976927,9.21130783 12.569304,9.21130783 C13.2409152,9.21130783 13.7846006,8.6676185 13.7846006,7.99600228 C13.7846006,7.32438606 13.2409152,6.78069676 12.569304,6.78069676 L12.569304,6.78069676 Z" id="形状"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -42,6 +42,8 @@ import pricingIcon from '@/assets/images/menu/pricing.svg'
import pricingActiveIcon from '@/assets/images/menu/pricing_active.svg'
import spaceConfigIcon from '@/assets/images/menu/spaceConfig.svg'
import spaceConfigActiveIcon from '@/assets/images/menu/spaceConfig_active.svg'
import ontologyIcon from '@/assets/images/menu/ontology.svg'
import ontologyActiveIcon from '@/assets/images/menu/ontology_active.svg'
// 图标路径映射表
const iconPathMap: Record<string, string> = {
@@ -73,6 +75,8 @@ const iconPathMap: Record<string, string> = {
'pricingActive': pricingActiveIcon,
'spaceConfig': spaceConfigIcon,
'spaceConfigActive': spaceConfigActiveIcon,
'ontology': ontologyIcon,
'ontologyActive': ontologyActiveIcon,
};
const { Sider } = Layout;
@@ -115,7 +119,7 @@ const Menu: FC<{
// 叶子节点
if (!subs || subs.length === 0) {
if (!menu.path) return null;
return {
key: menu.path,
title: menu.i18nKey ? t(menu.i18nKey) : menu.label,
@@ -124,13 +128,13 @@ const Menu: FC<{
{menu.i18nKey ? t(menu.i18nKey) : menu.label}
</span>
),
icon: iconSrc ? <img
src={iconSrc}
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
icon: iconSrc ? <img
src={iconSrc}
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
/> : null,
};
}
// 有子菜单的节点
const menuLabel = menu.i18nKey ? t(menu.i18nKey) : menu.label;
@@ -138,15 +142,15 @@ const Menu: FC<{
key: `submenu-${menu.id}`,
title: menuLabel,
label: menuLabel,
icon: iconSrc ? <img
src={iconSrc}
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
icon: iconSrc ? <img
src={iconSrc}
className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]"
/> : <UserOutlined/>,
children: generateMenuItems(subs),
};
}).filter(Boolean);
};
// 生成菜单项
const menuItems = generateMenuItems(menus);
// 初始加载菜单
@@ -164,17 +168,17 @@ const Menu: FC<{
for (const menu of menuList) {
if (menu.path) {
const menuPath = menu.path[0] !== '/' ? '/' + menu.path : menu.path;
// 精确匹配或路径前缀匹配(确保是完整路径段匹配)
const isExactMatch = menuPath === currentPath;
const isPrefixMatch = currentPath.startsWith(menuPath + '/') ||
currentPath === menuPath;
const isPrefixMatch = currentPath.startsWith(menuPath + '/') ||
currentPath === menuPath;
if (isExactMatch || isPrefixMatch) {
return { key: menu.path };
}
}
// 递归检查子菜单
if (menu.subs && menu.subs.length > 0) {
const newParentPaths = [...parentPaths, `submenu-${menu.id}`];
@@ -201,7 +205,7 @@ const Menu: FC<{
}
return (
<Sider
<Sider
width={240}
collapsedWidth={64}
collapsed={collapsed}
@@ -218,12 +222,12 @@ const Menu: FC<{
{t(`space.${storageType}`)}
</span>
</div>
: !collapsed
? <div className="rb:flex">
: !collapsed
? <div className="rb:flex">
<img src={logo} className={styles.logo} />
{t('title')}
</div>
: null
: null
}
<img src={collapsed ? menuUnfold : menuFold} className={styles.menuIcon} onClick={toggleSider} />
</div>

View File

@@ -112,7 +112,8 @@ export const en = {
pricing: 'Pricing Management',
orderPayment: 'Order Payment',
orderHistory: 'Order History',
spaceConfig: 'Space Configuration'
spaceConfig: 'Space Configuration',
ontology: 'Ontology Engineering',
},
dashboard: {
total_models: 'Available Models',
@@ -807,7 +808,8 @@ export const en = {
inactive: 'Inactive',
configurationName: 'Configuration Name',
emotionEngine: 'Emotion Engine',
reflectionEngine: 'Self-Reflection Engine'
reflectionEngine: 'Self-Reflection Engine',
scene_id: 'Ontology Scenario',
},
member: {
username: 'Username',
@@ -2355,6 +2357,34 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
question: 'Lessons Learned',
summary: 'Core Insights',
none: 'None'
}
},
ontology: {
searchPlaceholder: 'Search scenarios',
create: 'Create Project',
edit: 'Edit Project',
scene_name: 'Scenario Name',
scene_description: 'Scenario Description',
descriptionPlaceholder: 'Describe the purpose of this scenario and the entity types to extract',
typeCount: 'types',
created_at: 'Created At',
updated_at: 'Updated At',
entityTypes: 'Entity Types',
addClass: 'Add Type',
class_name: 'Type Name',
class_description: 'Type Definition',
classDescriptionPlaceholder: 'Describe the meaning and purpose of this type',
llm_id: 'Select Model',
scenario: 'Scenario Description',
scenarioPlaceholder: 'Please describe your business requirements',
run: 'Inference',
loadingConfirm: 'Inferring',
extractConfirm: 'Add Selected Types',
classType: 'Project Type',
extract: 'Project Inference',
source: 'Not Added',
target: 'Added',
},
},
};

View File

@@ -111,7 +111,8 @@ export const zh = {
pricing: '收费管理',
orderPayment: '订单支付',
orderHistory: '订单记录',
spaceConfig: '空间配置'
spaceConfig: '空间配置',
ontology: '本体工程',
},
knowledgeBase: {
home: '首页',
@@ -1172,7 +1173,8 @@ export const zh = {
inactive: '不活跃',
configurationName: '配置名称',
emotionEngine: '情感引擎',
reflectionEngine: '反思引擎'
reflectionEngine: '反思引擎',
scene_id: '本体场景',
},
member: {
username: '用户名',
@@ -2449,6 +2451,34 @@ export const zh = {
question: '踩过的坑',
summary: '核心洞察',
none: '无'
},
ontology: {
searchPlaceholder: '搜索场景',
create: '新增工程',
edit: '编辑工程',
scene_name: '场景名称',
scene_description: '场景描述',
descriptionPlaceholder: '描述该场景的用途和提取的实体类型',
typeCount: '个类型',
created_at: '创建时间',
updated_at: '更新时间',
entityTypes: '实体类型',
addClass: '添加类型',
class_name: '类型名称',
class_description: '类型定义',
classDescriptionPlaceholder: '描述该类型的含义和用途',
llm_id: '选择模型',
scenario: '场景描述',
scenarioPlaceholder: '请描述您的业务需求',
run: '推理',
loadingConfirm: '推断中',
extractConfirm: '添加选中类型',
classType: '工程类型',
extract: '工程推理',
source: '未添加项',
target: '已添加项',
}
},
}

View File

@@ -3,6 +3,7 @@ import { createHashRouter, createRoutesFromElements, Route } from 'react-router-
// 导入路由配置JSON
import routesConfig from './routes.json';
import Ontology from '@/views/Ontology';
// 递归函数,用于生成路由元素
@@ -68,6 +69,8 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
Pricing: lazy(() => import('@/views/Pricing')),
ToolManagement: lazy(() => import('@/views/ToolManagement')),
SpaceConfig: lazy(() => import('@/views/SpaceConfig')),
Ontology: lazy(() => import('@/views/Ontology')),
OntologyDetail: lazy(() => import('@/views/Ontology/pages/Detail')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),

View File

@@ -34,6 +34,7 @@
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
{ "path": "/space-config", "element": "SpaceConfig" },
{ "path": "/ontology", "element": "Ontology" },
{ "path": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
]
@@ -44,7 +45,8 @@
{ "path": "/application/config/:id", "element": "ApplicationConfig" },
{ "path": "/user-memory/neo4j/:id", "element": "Neo4jUserMemoryDetail" },
{ "path": "/statement/:id", "element": "StatementDetail" },
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" }
{ "path": "/user-memory/detail/:id/:type", "element": "MemoryNodeDetail" },
{ "path": "/ontology/:id", "element": "OntologyDetail" }
]
},
{

View File

@@ -332,6 +332,21 @@
}
]
},
{
"id": 21,
"parent": 0,
"code": "ontology",
"label": "本体工程",
"i18nKey": "menu.ontology",
"path": "/ontology",
"enable": true,
"display": true,
"level": 1,
"sort": 0,
"icon": null,
"iconActive": null,
"subs": null
},
{
"id": 10,
"parent": 0,

View File

@@ -0,0 +1,173 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App, Transfer, type TransferProps, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import type { OntologyClassData, ExtractData, OntologyClassExtractModalData, OntologyClassExtractModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { extractOntologyTypes, createOntologyClass } from '@/api/ontology'
import CustomSelect from '@/components/CustomSelect';
import { getModelListUrl } from '@/api/models'
import RbCard from '@/components/RbCard/Card';
import Tag from '@/components/Tag';
const FormItem = Form.Item;
interface OntologyClassExtractModalProps {
refresh: () => void;
}
const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, OntologyClassExtractModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<OntologyClassExtractModalData>();
const [loading, setLoading] = useState(false)
const [data, setData] = useState<OntologyClassData | null>(null)
const [extractData, setExtractData] = useState<ExtractData | null>(null)
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
const [selectedKeys, setSelectedKeys] = useState<TransferProps['selectedKeys']>([]);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
setData(null)
setExtractData(null)
};
const handleOpen = (vo: OntologyClassData) => {
form.resetFields();
setVisible(true);
setData(vo)
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
if (!data?.scene_id) return;
form
.validateFields()
.then((values) => {
setLoading(true)
extractOntologyTypes({
...values,
scene_id: data.scene_id,
domain: data.scene_name,
}).then((res) => {
const response = res as ExtractData
setExtractData(response)
setSelectedKeys([])
setTargetKeys(response.classes.map(vo => vo.id))
})
.finally(() => {
setLoading(false)
})
})
.catch((err) => {
console.log('err', err)
});
}
const handleConfirm = () => {
if (!extractData) {
handleSave()
} else {
if (!data?.scene_id) return;
if (!targetKeys || targetKeys.length === 0) {
message.warning(t('common.selectPlaceholder', { title: t('ontology.classType') }))
return
}
console.log('targetKeys', targetKeys)
createOntologyClass({
scene_id: data?.scene_id,
classes: extractData.classes.filter(vo => targetKeys?.includes(vo.id)).map(vo => ({ class_name: vo.name, class_description: vo.description }))
}).then(() => {
message.success(t('common.createSuccess'))
refresh()
handleClose()
}).finally(() => {
setLoading(false)
})
}
}
const onChange: TransferProps['onChange'] = (nextTargetKeys) => {
setTargetKeys(nextTargetKeys.filter(Boolean));
};
const onSelectChange: TransferProps['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
) => {
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys].filter(Boolean));
};
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('ontology.extract')}
open={visible}
onCancel={handleClose}
okText={extractData ? `${t('ontology.extractConfirm')}(${targetKeys?.length})` : loading ? t('ontology.loadingConfirm') : t('ontology.run')}
onOk={handleConfirm}
confirmLoading={loading}
okButtonProps={{ disabled: extractData !== null && targetKeys?.length === 0 }}
width={1000}
>
<Form
form={form}
layout="vertical"
>
<FormItem
name="llm_id"
label={t('ontology.llm_id')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
url={getModelListUrl}
valueKey="id"
labelKey="name"
hasAll={false}
placeholder={t('common.pleaseSelect')}
params={{ type: 'llm,chat', pagesize: 100, is_active: true }}
/>
</FormItem>
<FormItem
name="scenario"
label={t('ontology.scenario')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input.TextArea placeholder={t('ontology.scenarioPlaceholder')} />
</FormItem>
</Form>
{extractData && <RbCard
title={t('ontology.classType')}
bodyClassName='rb:flex rb:justify-center rb:h-[450px]!'
>
<Transfer
titles={[t('ontology.source'), t('ontology.target')]}
dataSource={extractData?.classes?.map(vo => ({ ...vo, key: vo.id }))}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={onChange}
onSelectChange={onSelectChange}
render={(item) => (<div>
{item.name}
<Flex wrap gap={8}>{item.examples.map((vo, index) => <Tag color="default" key={index}>{vo}</Tag>)}</Flex>
</div>)}
listStyle={{ width: '400px', height: '100%' }}
/>
</RbCard>}
</RbModal>
);
});
export default OntologyClassExtractModal;

View File

@@ -0,0 +1,96 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { AddClassItem, OntologyClassModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { createOntologyClass } from '@/api/ontology'
const FormItem = Form.Item;
interface OntologyClassModalProps {
refresh: () => void;
}
const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<AddClassItem>();
const [loading, setLoading] = useState(false)
const [scene_id, setSceneId] = useState<string | null>(null)
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
const handleOpen = (scene_id: string) => {
form.resetFields();
setVisible(true);
setSceneId(scene_id)
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
if (!scene_id) return;
form
.validateFields()
.then((values) => {
setLoading(true)
createOntologyClass({
scene_id: scene_id,
classes: [{ ...values }]
}).then(() => {
message.success(t('common.saveSuccess'));
handleClose();
refresh();
})
.finally(() => setLoading(false))
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={t('ontology.addClass')}
open={visible}
onCancel={handleClose}
okText={t('common.create')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<FormItem
name="class_name"
label={t('ontology.class_name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
<FormItem
name="class_description"
label={t('ontology.class_description')}
>
<Input.TextArea placeholder={t('ontology.classDescriptionPlaceholder')} />
</FormItem>
</Form>
</RbModal>
);
});
export default OntologyClassModal;

View File

@@ -0,0 +1,99 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { OntologyItem, OntologyModalData, OntologyModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { createOntologyScene, updateOntologyScene } from '@/api/ontology'
const FormItem = Form.Item;
interface OntologyModalProps {
refresh: () => void;
}
const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [editVo, setEditVo] = useState<OntologyItem | null>(null)
const [form] = Form.useForm<OntologyModalData>();
const [loading, setLoading] = useState(false)
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
setEditVo(null)
};
const handleOpen = (vo?: OntologyItem) => {
if (vo) {
setEditVo(vo);
form.setFieldsValue(vo);
} else {
form.resetFields();
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
form
.validateFields()
.then((values) => {
setLoading(true)
const request = editVo?.scene_id ? updateOntologyScene(editVo.scene_id, values) : createOntologyScene(values)
request
.then(() => {
message.success(t('common.saveSuccess'));
handleClose();
refresh();
})
.finally(() => setLoading(false))
})
.catch((err) => {
console.log('err', err)
});
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
}));
return (
<RbModal
title={editVo?.scene_id ? t('ontology.edit') : t('ontology.create')}
open={visible}
onCancel={handleClose}
okText={editVo?.scene_id ? t('common.save') : t('common.create')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<FormItem
name="scene_name"
label={t('ontology.scene_name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
<FormItem
name="scene_description"
label={t('ontology.scene_description')}
>
<Input.TextArea placeholder={t('ontology.descriptionPlaceholder')} />
</FormItem>
</Form>
</RbModal>
);
});
export default OntologyModal;

View File

@@ -0,0 +1,45 @@
import { type FC, type ReactNode } from 'react';
import { useNavigate } from 'react-router-dom';
import { Layout, Button } from 'antd';
import { useTranslation } from 'react-i18next';
import logoutIcon from '@/assets/images/logout_hover.svg'
const { Header } = Layout;
interface ConfigHeaderProps {
name?: string;
subTitle?: ReactNode | string;
extra?: ReactNode;
}
const PageHeader: FC<ConfigHeaderProps> = ({
name,
subTitle,
extra
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const goBack = () => {
navigate(-1)
}
return (
<Header className="rb:w-full rb:h-16 rb:flex rb:justify-between rb:p-[0_16px_0_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
<div className="rb:flex rb:flex-col rb:justify-center rb:gap-1 rb:mr-4">
<div className="rb:text-[16px] rb:leading-6 rb:font-medium">
{name}
</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4">{subTitle}</div>
</div>
<div className="rb:flex rb:items-center rb:gap-3">
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={goBack}>
<img src={logoutIcon} className="rb:w-4 rb:h-4" />
{t('common.return')}
</Button>
{extra}
</div>
</Header>
);
};
export default PageHeader;

View File

@@ -0,0 +1,133 @@
import { type FC, useState, useRef, type MouseEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Row, Col, Button, Flex, Divider, Space, App, Tooltip } from 'antd'
import SearchInput from '@/components/SearchInput';
import OntologyModal from './components/OntologyModal'
import type { OntologyModalRef, OntologyItem, Query } from './types'
import RbCard from '@/components/RbCard/Card'
import Tag from '@/components/Tag'
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { getOntologyScenesUrl, deleteOntologyScene } from '@/api/ontology'
import { formatDateTime } from '@/utils/format'
const Ontology: FC = () => {
const { t } = useTranslation();
const navigate = useNavigate()
const { modal, message } = App.useApp();
const [query, setQuery] = useState<Query>({});
const scrollListRef = useRef<PageScrollListRef>(null)
const entityModalRef = useRef<OntologyModalRef>(null)
const handleCreate = () => {
entityModalRef.current?.handleOpen()
}
const handleEdit = (record: OntologyItem, e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
entityModalRef.current?.handleOpen(record)
}
const handleDelete = (item: OntologyItem, e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.scene_name }),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
deleteOntologyScene(item.scene_id)
.then(() => {
message.success(t('common.deleteSuccess'))
scrollListRef.current?.refresh()
})
}
})
}
const handleJump = (record: OntologyItem) => {
navigate(`/ontology/${record.scene_id}`)
}
return (
<>
<Row gutter={16} className="rb:mb-4">
<Col span={8}>
<SearchInput
placeholder={t('ontology.searchPlaceholder')}
onSearch={(value) => setQuery({ scene_name: value })}
className="rb:w-full!"
/>
</Col>
<Col span={16} className="rb:text-right">
<Button type="primary" onClick={handleCreate}>
+ {t('ontology.create')}
</Button>
</Col>
</Row>
<PageScrollList<OntologyItem, Query>
ref={scrollListRef}
url={getOntologyScenesUrl}
query={query}
column={3}
renderItem={(item) =>(
<RbCard
title={item.scene_name}
extra={<Tag>{item.type_num} {t('ontology.typeCount')}</Tag>}
onClick={() => handleJump(item)}
className="rb:cursor-pointer"
>
<div
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<span className="rb:whitespace-nowrap">{t(`ontology.scene_description`)}</span>
<Tooltip title={item.scene_description} placement="topRight">
<span className="rb:font-medium rb:flex-1 rb:text-right rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">{item.scene_description}</span>
</Tooltip>
</div>
{(['created_at', 'updated_at'] as const).map(key => (
<div
key={key}
className="rb:flex rb:gap-2 rb:justify-between rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
>
<span className="rb:whitespace-nowrap">{t(`ontology.${key}`)}</span>
<span className="rb:font-medium">{formatDateTime(item[key])}</span>
</div>
))}
<Divider size="middle" />
<Flex gap={8} wrap>
<div className="rb:text-[#5B6167] rb:leading-4.5">{t('ontology.entityTypes')}: </div>
{item.entity_type?.map((type, i) => (
<Tag key={i} color={i % 2 ? 'processing' : 'success'}>{type}</Tag>
))}
{item.type_num > 3 && (
<Tag color="default">+{item.type_num - 3}</Tag>
)}
</Flex>
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
<Space size={16}>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
onClick={(e) => handleEdit(item, e)}
></div>
<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={(e) => handleDelete(item, e)}
></div>
</Space>
</div>
</RbCard>
)}
/>
<OntologyModal
ref={entityModalRef}
refresh={() => scrollListRef.current?.refresh()}
/>
</>
)
}
export default Ontology

View File

@@ -0,0 +1,122 @@
import { type FC, useEffect, useState, useRef } from 'react'
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { App, Row, Col, Tooltip, Space, Button } from 'antd'
import PageHeader from '../components/PageHeader'
import { getOntologyClassList, deleteOntologyClass } from '@/api/ontology'
import type { OntologyClassData, OntologyClassModalRef, OntologyClassExtractModalRef, OntologyClassItem } from '@/views/Ontology/types'
import RbCard from '@/components/RbCard/Card';
import OntologyClassModal from '../components/OntologyClassModal'
import SearchInput from '@/components/SearchInput';
import OntologyClassExtractModal from '../components/OntologyClassExtractModal'
import BodyWrapper from '@/components/Empty/BodyWrapper'
const Detail: FC = () => {
const { t } = useTranslation();
const { id } = useParams()
const { modal, message } = App.useApp()
const ontologyClassModalRef = useRef<OntologyClassModalRef>(null)
const ontologyClassExtractModalRef = useRef<OntologyClassExtractModalRef>(null)
const [query, setQuery] = useState<{
class_name?: string;
}>({});
const [loading, setLoading] = useState(false)
const [data, setData] = useState<OntologyClassData>({} as OntologyClassData)
useEffect(() => {
getData()
}, [id, query])
const getData = () => {
if (!id) return;
setLoading(true)
getOntologyClassList({
...query,
scene_id: id
})
.then(res => {
setData(res as OntologyClassData)
})
.finally(() => {
setLoading(false)
})
}
const handleDelete = (item: OntologyClassItem) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.class_name }),
okText: t('common.delete'),
cancelText: t('common.cancel'),
okType: 'danger',
onOk: () => {
deleteOntologyClass(item.class_id)
.then(() => {
getData();
message.success(t('common.deleteSuccess'))
})
}
})
}
const handleAdd = () => {
ontologyClassModalRef.current?.handleOpen(data.scene_id)
}
const handleExtract = () => {
ontologyClassExtractModalRef.current?.handleOpen(data)
}
return (
<>
<PageHeader
name={data.scene_name}
subTitle={<div>{data.scene_description}</div>}
extra={<Space>
<Button type="primary" ghost className="rb:h-6! rb:px-2! rb:leading-5.5!" onClick={handleAdd}>+ {t('ontology.addClass')}</Button>
<Button className="rb:h-6! rb:px-2! rb:leading-5.5!" type="primary" onClick={handleExtract}>+ {t('ontology.extract')}</Button>
</Space>}
/>
<div className="rb:h-[calc(100vh-64px)] rb:overflow-y-auto rb:py-3 rb:px-4">
<Row gutter={16} className="rb:mb-4">
<Col span={6} offset={18}>
<SearchInput
placeholder={t('ontology.searchPlaceholder')}
onSearch={(value) => setQuery({ class_name: value })}
className="rb:w-full!"
/>
</Col>
</Row>
<BodyWrapper loading={loading} empty={!data.items?.length}>
<Row gutter={[16, 16]}>
{data.items?.map(item => (
<Col key={item.class_id} span={6}>
<RbCard
title={item.class_name}
extra={<div
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDelete(item)}
></div>}
className="rb:bg-transparent!"
>
<Tooltip title={item.class_description}>
<div className="rb:h-8.5 rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-2">{item.class_description}</div>
</Tooltip>
</RbCard>
</Col>
))}
</Row>
</BodyWrapper>
</div>
<OntologyClassModal
ref={ontologyClassModalRef}
refresh={getData}
/>
<OntologyClassExtractModal
ref={ontologyClassExtractModalRef}
refresh={getData}
/>
</>
)
}
export default Detail

View File

@@ -0,0 +1,79 @@
export interface Query {
pagesize?: number;
page?: number;
scene_name?: string;
}
export interface OntologyItem {
scene_id: string;
scene_name: string;
scene_description: string;
type_num: number;
entity_type: string[];
workspace_id: string;
created_at: number;
updated_at: number;
classes_count: number;
}
export interface OntologyModalData {
scene_name: string;
scene_description: string;
}
export interface OntologyModalRef {
handleOpen: (data?: OntologyItem) => void;
}
export interface OntologyClassItem {
class_id: string;
class_name: string;
class_description: string;
scene_id: string;
created_at: number;
updated_at: number;
}
export interface OntologyClassData {
total: number;
scene_id: string;
scene_name: string;
scene_description: string;
items: OntologyClassItem[];
}
export interface AddClassItem {
class_name: string;
class_description: string;
}
export interface OntologyClassModalData {
scene_id: string;
classes: AddClassItem[]
}
export interface OntologyClassModalRef {
handleOpen: (scene_id: string) => void;
}
export interface OntologyClassExtractModalData {
llm_id: string;
scene_id: string;
scenario: string;
domain: string; // scene_name
}
export interface OntologyClassExtractModalRef {
handleOpen: (vo: OntologyClassData) => void;
}
export interface ExtractClassItem {
id: string;
name: string;
name_chinese: string;
description: string;
examples: string[];
parent_class: string | null;
entity_type: string;
domain: string;
}
export interface ExtractData {
domain: string;
extracted_count: number;
classes: ExtractClassItem[]
}