Merge pull request #256 from SuanmoSuanyangTechnology/feature/ontology_zy
Feature/ontology zy
This commit is contained in:
39
web/src/api/ontology.ts
Normal file
39
web/src/api/ontology.ts
Normal 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}`)
|
||||
}
|
||||
11
web/src/assets/images/menu/ontology.svg
Normal file
11
web/src/assets/images/menu/ontology.svg
Normal 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 |
11
web/src/assets/images/menu/ontology_active.svg
Normal file
11
web/src/assets/images/menu/ontology_active.svg
Normal 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 |
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '我的历史',
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// 应用数据类型
|
||||
export interface Query {
|
||||
search: string;
|
||||
}
|
||||
export interface Application {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}`)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
// 定义组件暴露的方法接口
|
||||
|
||||
173
web/src/views/Ontology/components/OntologyClassExtractModal.tsx
Normal file
173
web/src/views/Ontology/components/OntologyClassExtractModal.tsx
Normal 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;
|
||||
96
web/src/views/Ontology/components/OntologyClassModal.tsx
Normal file
96
web/src/views/Ontology/components/OntologyClassModal.tsx
Normal 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;
|
||||
99
web/src/views/Ontology/components/OntologyModal.tsx
Normal file
99
web/src/views/Ontology/components/OntologyModal.tsx
Normal 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;
|
||||
45
web/src/views/Ontology/components/PageHeader.tsx
Normal file
45
web/src/views/Ontology/components/PageHeader.tsx
Normal 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;
|
||||
133
web/src/views/Ontology/index.tsx
Normal file
133
web/src/views/Ontology/index.tsx
Normal 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
|
||||
122
web/src/views/Ontology/pages/Detail.tsx
Normal file
122
web/src/views/Ontology/pages/Detail.tsx
Normal 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
|
||||
79
web/src/views/Ontology/types.ts
Normal file
79
web/src/views/Ontology/types.ts
Normal 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[]
|
||||
}
|
||||
Reference in New Issue
Block a user