Merge pull request #256 from SuanmoSuanyangTechnology/feature/ontology_zy

Feature/ontology zy
This commit is contained in:
yingzhao
2026-01-30 14:26:49 +08:00
committed by GitHub
24 changed files with 980 additions and 56 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

@@ -1,6 +1,6 @@
import type { FC, ReactNode } from 'react'
import { Skeleton } from 'antd'
import Empty from './index'
import PageEmpty from './PageEmpty'
import PageLoading from './PageLoading'
interface BodyWrapperProps {
children: ReactNode
@@ -9,10 +9,10 @@ interface BodyWrapperProps {
}
const BodyWrapper: FC<BodyWrapperProps> = ({ children, loading = false, empty }) => {
if (loading) {
return <Skeleton active />
return <PageLoading />
}
if (!loading && empty) {
return <Empty />
return <PageEmpty />
}
return children
}

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { List, Skeleton} from 'antd';
import { List } from 'antd';
import InfiniteScroll from 'react-infinite-scroll-component';
import { request } from '@/utils/request';
import Empty from '@/components/Empty';
import PageEmpty from '@/components/Empty/PageEmpty'
import PageLoading from '@/components/Empty/PageLoading'
const PAGE_SIZE = 20;
interface ApiResponse {
items?: Record<string, unknown>[];
interface ApiResponse<T> {
items?: T[];
page: {
page: number;
pagesize: number;
@@ -19,26 +20,25 @@ export interface PageScrollListRef {
refresh: () => void;
}
interface PageScrollListProps {
interface PageScrollListProps<T, Q = Record<string, unknown>> {
url: string;
renderItem: (item: Record<string, unknown>) => React.ReactNode;
query?: Record<string, unknown>;
renderItem: (item: T) => React.ReactNode;
query?: Q;
column?: number;
className?: string;
}
const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
renderItem,
query,
url,
column = 4,
className = '',
}, ref) => {
}: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
useImperativeHandle(ref, () => ({
refresh,
}));
const [loading, setLoading] = useState(false);
const [data, setData] = useState<Record<string, unknown>[]>([]);
const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null);
@@ -54,8 +54,8 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
...(query||{}),
})
.then((res) => {
const response = res as ApiResponse;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : [];
const response = res as ApiResponse<T>;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response as T[] : [];
if (flag) {
setData(results);
} else {
@@ -104,7 +104,7 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
dataLength={data.length}
next={loadMoreData}
hasMore={hasMore}
loader={<Skeleton active />}
loader={<PageLoading />}
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
scrollableTarget="scrollableDiv"
>
@@ -118,11 +118,11 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
</List.Item>
)}
/>
) : !loading ? <Empty /> : null}
) : !loading ? <PageEmpty /> : null}
</InfiniteScroll>
</div>
</>
);
});
}) as <T = Record<string, unknown>, Q = Record<string, unknown>>(props: PageScrollListProps<T, Q> & { ref?: React.Ref<PageScrollListRef> }) => React.ReactElement;
export default PageScrollList;

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'
import promptIcon from '@/assets/images/menu/prompt.svg'
import promptActiveIcon from '@/assets/images/menu/prompt_active.svg'
@@ -75,6 +77,8 @@ const iconPathMap: Record<string, string> = {
'pricingActive': pricingActiveIcon,
'spaceConfig': spaceConfigIcon,
'spaceConfigActive': spaceConfigActiveIcon,
'ontology': ontologyIcon,
'ontologyActive': ontologyActiveIcon,
'prompt': promptIcon,
'promptActive': promptActiveIcon,
};
@@ -119,7 +123,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,
@@ -128,13 +132,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;
@@ -142,15 +146,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);
// 初始加载菜单
@@ -168,17 +172,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}`];
@@ -205,7 +209,7 @@ const Menu: FC<{
}
return (
<Sider
<Sider
width={240}
collapsedWidth={64}
collapsed={collapsed}
@@ -222,12 +226,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

@@ -113,6 +113,7 @@ export const en = {
orderPayment: 'Order Payment',
orderHistory: 'Order History',
spaceConfig: 'Space Configuration',
ontology: 'Ontology Engineering',
prompt: 'Prompt Engineering',
},
dashboard: {
@@ -869,7 +870,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',
@@ -2437,6 +2439,33 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
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',
prompt: {
editor: 'Prompt Generator',
history: 'My History',

View File

@@ -112,6 +112,7 @@ export const zh = {
orderPayment: '订单支付',
orderHistory: '订单记录',
spaceConfig: '空间配置',
ontology: '本体工程',
prompt: '提示词工程',
},
knowledgeBase: {
@@ -1240,7 +1241,8 @@ export const zh = {
inactive: '不活跃',
configurationName: '配置名称',
emotionEngine: '情感引擎',
reflectionEngine: '反思引擎'
reflectionEngine: '反思引擎',
scene_id: '本体场景',
},
member: {
username: '用户名',
@@ -2526,6 +2528,34 @@ export const zh = {
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: '已添加项',
}
prompt: {
editor: '提示词生成器',
history: '我的历史',

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')),
Prompt: lazy(() => import('@/views/Prompt')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),

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": "/prompt", "element": "Prompt" },
{ "path": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
@@ -45,7 +46,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

@@ -58,13 +58,12 @@ const ApiKeyManagement: React.FC = () => {
</Button>
</div>
<PageScrollList
<PageScrollList<ApiKey, { is_active: boolean; type: string }>
ref={scrollListRef}
url={getApiKeyListUrl}
query={{ is_active: true, type: 'service' }}
column={2}
renderItem={(item: Record<string, unknown>) => {
let apiKeyItem = item as unknown as ApiKey
renderItem={(apiKeyItem) => {
return (
<RbCard
title={apiKeyItem.name}

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App } from 'antd';
import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons';
import type { Application, ApplicationModalRef } from './types';
import type { Application, ApplicationModalRef, Query } from './types';
import ApplicationModal from './components/ApplicationModal';
import SearchInput from '@/components/SearchInput'
import RbCard from '@/components/RbCard/Card'
@@ -14,7 +14,7 @@ import { formatDateTime } from '@/utils/format';
const ApplicationManagement: React.FC = () => {
const { t } = useTranslation();
const { modal } = App.useApp();
const [query, setQuery] = useState({});
const [query, setQuery] = useState<Query>({} as Query);
const applicationModalRef = useRef<ApplicationModalRef>(null);
const scrollListRef = useRef<PageScrollListRef>(null)
@@ -47,7 +47,7 @@ const ApplicationManagement: React.FC = () => {
}
return (
<>
<Row gutter={16} className="rb:mb-[16px]">
<Row gutter={16} className="rb:mb-4">
<Col span={12}>
<SearchInput
placeholder={t('application.searchPlaceholder')}
@@ -62,22 +62,22 @@ const ApplicationManagement: React.FC = () => {
</Col>
</Row>
<PageScrollList
<PageScrollList<Application, Query>
ref={scrollListRef}
url={getApplicationListUrl}
query={query}
renderItem={(item: Application) => (
renderItem={(item) => (
<RbCard
title={item.name}
avatar={
<div className="rb:w-[48px] rb:h-[48px] rb:rounded-[8px] rb:mr-[13px] rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
<div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]}
</div>
}
>
{['type', 'source', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-[20px] rb:font-regular rb:text-[14px]", {
'rb:mt-[12px]': index !== 0
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-3': index !== 0
})}>
<span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span>
<span className={clsx({
@@ -89,14 +89,14 @@ const ApplicationManagement: React.FC = () => {
: key === 'source' && !item.is_shared
? t('application.configuration')
: key === 'created_at'
? formatDateTime(item[key as keyof Application], 'YYYY-MM-DD HH:mm:ss')
? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
: t(`application.${item[key as keyof Application]}`)
}
</span>
</div>
))}
<div className="rb:mt-[20px] rb:flex rb:justify-between rb:gap-[10px]">
<div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
<Button type="primary" ghost className="rb:w-[calc(100%-46px)]" onClick={() => handleEdit(item)}>{t('application.configuration')}</Button>
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(item)}></Button>
</div>

View File

@@ -1,4 +1,7 @@
// 应用数据类型
export interface Query {
search: string;
}
export interface Application {
id: string;
workspace_id: string;

View File

@@ -1,9 +1,12 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { MemoryFormData, Memory, MemoryFormRef } from '../types';
import RbModal from '@/components/RbModal'
import { createMemoryConfig, updateMemoryConfig } from '@/api/memory'
import { getOntologyScenesUrl } from '@/api/ontology'
import CustomSelect from '@/components/CustomSelect';
const FormItem = Form.Item;
@@ -38,6 +41,7 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
form.setFieldsValue({
config_name: memory.config_name,
config_desc: memory.config_desc,
scene_id: memory.scene_id
});
} else {
form.resetFields();
@@ -102,6 +106,21 @@ const MemoryForm = forwardRef<MemoryFormRef, MemoryFormProps>(({
>
<Input.TextArea placeholder={t('common.pleaseEnter')} />
</FormItem>
<Form.Item
name="scene_id"
label={t('memory.scene_id')}
rules={[{ required: true, message: t('common.pleaseSelect') }]}
>
<CustomSelect
placeholder={t('common.pleaseSelect')}
url={getOntologyScenesUrl}
params={{ pagesize: 100, page: 1 }}
hasAll={false}
valueKey='scene_id'
labelKey="scene_name"
/>
</Form.Item>
</Form>
</RbModal>
);

View File

@@ -10,6 +10,7 @@ import { getMemoryConfigList, deleteMemoryConfig } from '@/api/memory'
import BodyWrapper from '@/components/Empty/BodyWrapper'
import { formatDateTime } from '@/utils/format';
import clsx from 'clsx'
import RbAlert from '@/components/RbAlert'
const MemoryManagement: React.FC = () => {
const { t } = useTranslation();
@@ -98,10 +99,18 @@ const MemoryManagement: React.FC = () => {
<Tooltip title={item.config_desc}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4.25 rb:font-regular rb:-mt-1 rb:wrap-break-word rb:line-clamp-1">{item.config_desc}</div>
</Tooltip>
<RbAlert className="rb:mt-3 ">
<div className={clsx("rb:flex rb:gap-5 rb:font-regular rb:text-[14px]")}>
<span className="rb:text-[#5B6167]">{t('memory.scene_id')}: </span>
<span className="rb:font-medium">
{item.scene_name || '-'}
</span>
</div>
</RbAlert>
<div className="rb:grid rb:grid-cols-2 rb:gap-4 rb:mt-3">
<div className="rb:grid rb:grid-cols-2 rb:gap-x-4 rb:gap-y-3 rb:mt-3">
{['memoryExtractionEngine', 'forgottenEngine', 'emotionEngine', 'reflectionEngine'].map((key) => (
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-10 rb:rounded-md rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:mt-3 rb:text-[#5B6167] rb:font-medium"
<div key={key} className="rb:group rb:cursor-pointer rb:bg-[#F0F3F8] rb:h-10 rb:rounded-md rb:flex rb:items-center rb:justify-between rb:p-[0_8px_0_12px] rb:text-[#5B6167] rb:font-medium"
onClick={() => handleClick(item.config_id, key)}
>
{t(`memory.${key}`)}

View File

@@ -3,6 +3,7 @@ export interface MemoryFormData {
config_id?: number;
config_name: string;
config_desc?: string;
scene_id?: string;
}
// 内存数据类型
@@ -29,6 +30,8 @@ export interface Memory {
updated_at: string;
config_desc: string;
workspace_id: string;
scene_id: string;
scene_name: string;
[key: string]: string | number | boolean;
}
// 定义组件暴露的方法接口

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[]
}