mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-19 10:07:24 +00:00
feat: kb ui
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { GET, POST, PUT, DELETE } from '../request';
|
import { GET, POST, PUT, DELETE } from '../request';
|
||||||
import type { KbItemType } from '@/types/plugin';
|
import type { KbItemType, KbListItemType } from '@/types/plugin';
|
||||||
import { RequestPaging } from '@/types/index';
|
import { RequestPaging } from '@/types/index';
|
||||||
import { TrainingModeEnum } from '@/constants/plugin';
|
import { TrainingModeEnum } from '@/constants/plugin';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Props as SearchTestProps,
|
Props as SearchTestProps,
|
||||||
Response as SearchTestResponse
|
Response as SearchTestResponse
|
||||||
} from '@/pages/api/openapi/kb/searchTest';
|
} from '@/pages/api/openapi/kb/searchTest';
|
||||||
|
import { Response as KbDataItemType } from '@/pages/api/plugins/kb/data/getDataById';
|
||||||
|
|
||||||
export type KbUpdateParams = {
|
export type KbUpdateParams = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -19,7 +20,7 @@ export type KbUpdateParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* knowledge base */
|
/* knowledge base */
|
||||||
export const getKbList = () => GET<KbItemType[]>(`/plugins/kb/list`);
|
export const getKbList = () => GET<KbListItemType[]>(`/plugins/kb/list`);
|
||||||
|
|
||||||
export const getKbById = (id: string) => GET<KbItemType>(`/plugins/kb/detail?id=${id}`);
|
export const getKbById = (id: string) => GET<KbItemType>(`/plugins/kb/detail?id=${id}`);
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ export const getTrainingData = (data: { kbId: string; init: boolean }) =>
|
|||||||
}>(`/plugins/kb/data/getTrainingData`, data);
|
}>(`/plugins/kb/data/getTrainingData`, data);
|
||||||
|
|
||||||
export const getKbDataItemById = (dataId: string) =>
|
export const getKbDataItemById = (dataId: string) =>
|
||||||
GET(`/plugins/kb/data/getDataById`, { dataId });
|
GET<KbDataItemType>(`/plugins/kb/data/getDataById`, { dataId });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 直接push数据
|
* 直接push数据
|
||||||
|
@@ -1 +1 @@
|
|||||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688883742264" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1872" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M848.57995022 110.69313539a64.72691317 64.72691317 0 0 1 64.72691439 64.72691439v673.15990044a64.72691317 64.72691317 0 0 1-64.72691439 64.72691439H175.42004978A64.72691317 64.72691317 0 0 1 110.69313539 848.57995022V175.42004978A64.72691317 64.72691317 0 0 1 175.42004978 110.69313539h673.15990044M848.57995022 45.96622222H175.42004978a129.45382756 129.45382756 0 0 0-129.45382756 129.45382756v673.15990044a129.45382756 129.45382756 0 0 0 129.45382756 129.45382756h673.15990044a129.45382756 129.45382756 0 0 0 129.45382756-129.45382756V175.42004978a129.45382756 129.45382756 0 0 0-129.45382756-129.45382756z" p-id="1873"></path><path d="M520.54395259 751.48957984A32.36345719 32.36345719 0 0 1 488.18049539 719.12612386V434.3277037a32.36345719 32.36345719 0 0 1 64.72691439 0v284.79842016a32.36345719 32.36345719 0 0 1-32.36345719 32.36345598zM304.87387614 751.48957984A32.36345719 32.36345719 0 0 1 272.51042016 719.12612386v-155.34459259a32.36345719 32.36345719 0 0 1 64.72691317 0v155.34459259A32.36345719 32.36345719 0 0 1 304.87387614 751.48957984zM719.12612386 751.48957984A32.36345719 32.36345719 0 0 1 686.76266667 719.12612386V304.87387614a32.36345719 32.36345719 0 0 1 64.72691317 0v414.25224772A32.36345719 32.36345719 0 0 1 719.12612386 751.48957984z" p-id="1874"></path></svg>
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1689305725826" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3372" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M512 774.4a38.4 38.4 0 0 1-38.4-38.4V304a38.4 38.4 0 0 1 76.8 0v432a38.4 38.4 0 0 1-38.4 38.4zM741.12 774.4a38.4 38.4 0 0 1-38.4-38.4V501.12a38.4 38.4 0 1 1 76.8 0v234.88a38.4 38.4 0 0 1-38.4 38.4zM282.88 774.4a38.4 38.4 0 0 1-38.4-38.4V550.4A38.4 38.4 0 0 1 320 550.4v185.6a38.4 38.4 0 0 1-37.12 38.4zM282.88 421.12a38.4 38.4 0 0 1-38.4-38.4V304a38.4 38.4 0 0 1 76.8 0V384a38.4 38.4 0 0 1-38.4 37.12z" p-id="3373"></path><path d="M869.76 140.8a76.8 76.8 0 0 1 76.8 76.8v588.8a76.8 76.8 0 0 1-76.8 76.8H154.24a76.8 76.8 0 0 1-76.8-76.8V217.6a76.8 76.8 0 0 1 76.8-76.8h715.52m0-76.8H154.24A153.6 153.6 0 0 0 0 217.6v588.8A153.6 153.6 0 0 0 154.24 960h715.52A153.6 153.6 0 0 0 1024 806.4V217.6A153.6 153.6 0 0 0 869.76 64z" p-id="3374"></path></svg>
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -38,8 +38,8 @@ const Navbar = ({ unread }: { unread: number }) => {
|
|||||||
label: '知识库',
|
label: '知识库',
|
||||||
icon: 'dbLight',
|
icon: 'dbLight',
|
||||||
activeIcon: 'dbFill',
|
activeIcon: 'dbFill',
|
||||||
link: `/kb`,
|
link: `/kb/list`,
|
||||||
activeLink: ['/kb']
|
activeLink: ['/kb/list', '/kb/detail']
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '市场',
|
label: '市场',
|
||||||
|
@@ -7,6 +7,7 @@ import { getVector } from '../plugin/vector';
|
|||||||
import type { KbTestItemType } from '@/types/plugin';
|
import type { KbTestItemType } from '@/types/plugin';
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
|
model: string;
|
||||||
kbId: string;
|
kbId: string;
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
@@ -14,9 +15,9 @@ export type Response = KbTestItemType['results'];
|
|||||||
|
|
||||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
const { kbId, text } = req.body as Props;
|
const { kbId, text, model } = req.body as Props;
|
||||||
|
|
||||||
if (!kbId || !text) {
|
if (!kbId || !text || !model) {
|
||||||
throw new Error('缺少参数');
|
throw new Error('缺少参数');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +28,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
throw new Error('缺少用户ID');
|
throw new Error('缺少用户ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
const vector = await getVector({
|
const { vectors } = await getVector({
|
||||||
|
model,
|
||||||
userId,
|
userId,
|
||||||
input: [text]
|
input: [text]
|
||||||
});
|
});
|
||||||
@@ -36,9 +38,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
|||||||
`BEGIN;
|
`BEGIN;
|
||||||
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
|
||||||
select id,q,a,source,(vector <#> '[${
|
select id,q,a,source,(vector <#> '[${
|
||||||
vector[0]
|
vectors[0]
|
||||||
}]') * -1 AS score from modelData where kb_id='${kbId}' AND user_id='${userId}' order by vector <#> '[${
|
}]') * -1 AS score from modelData where kb_id='${kbId}' AND user_id='${userId}' order by vector <#> '[${
|
||||||
vector[0]
|
vectors[0]
|
||||||
}]' limit 12;
|
}]' limit 12;
|
||||||
COMMIT;`
|
COMMIT;`
|
||||||
);
|
);
|
||||||
|
@@ -5,6 +5,13 @@ import { authUser } from '@/service/utils/auth';
|
|||||||
import { PgClient } from '@/service/pg';
|
import { PgClient } from '@/service/pg';
|
||||||
import type { KbDataItemType } from '@/types/plugin';
|
import type { KbDataItemType } from '@/types/plugin';
|
||||||
|
|
||||||
|
export type Response = {
|
||||||
|
id: string;
|
||||||
|
q: string;
|
||||||
|
a: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
let { dataId } = req.query as {
|
let { dataId } = req.query as {
|
||||||
|
@@ -18,10 +18,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
const data = await KB.findOne({
|
const data = await KB.findOne(
|
||||||
_id: id,
|
{
|
||||||
userId
|
_id: id,
|
||||||
});
|
userId
|
||||||
|
},
|
||||||
|
'_id avatar name userId tags'
|
||||||
|
);
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error('kb is not exist');
|
throw new Error('kb is not exist');
|
||||||
@@ -33,7 +36,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
avatar: data.avatar,
|
avatar: data.avatar,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
userId: data.userId,
|
userId: data.userId,
|
||||||
updateTime: data.updateTime,
|
|
||||||
tags: data.tags.join(' ')
|
tags: data.tags.join(' ')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -2,8 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||||||
import { jsonRes } from '@/service/response';
|
import { jsonRes } from '@/service/response';
|
||||||
import { connectToDatabase, KB } from '@/service/mongo';
|
import { connectToDatabase, KB } from '@/service/mongo';
|
||||||
import { authUser } from '@/service/utils/auth';
|
import { authUser } from '@/service/utils/auth';
|
||||||
import { PgClient } from '@/service/pg';
|
import { KbListItemType } from '@/types/plugin';
|
||||||
import { KbItemType } from '@/types/plugin';
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||||
try {
|
try {
|
||||||
@@ -12,25 +11,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
|
||||||
const kbList = await KB.find({
|
const kbList = await KB.find(
|
||||||
userId
|
{
|
||||||
}).sort({ updateTime: -1 });
|
userId
|
||||||
|
},
|
||||||
|
'_id avatar name tags'
|
||||||
|
).sort({ updateTime: -1 });
|
||||||
|
|
||||||
const data = await Promise.all(
|
const data = await Promise.all(
|
||||||
kbList.map(async (item) => ({
|
kbList.map(async (item) => ({
|
||||||
_id: item._id,
|
_id: item._id,
|
||||||
avatar: item.avatar,
|
avatar: item.avatar,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
userId: item.userId,
|
tags: item.tags
|
||||||
updateTime: item.updateTime,
|
|
||||||
tags: item.tags.join(' '),
|
|
||||||
totalData: await PgClient.count('modelData', {
|
|
||||||
where: [['user_id', userId], 'AND', ['kb_id', item._id]]
|
|
||||||
})
|
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
jsonRes<KbItemType[]>(res, {
|
jsonRes<KbListItemType[]>(res, {
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
useTheme
|
useTheme
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import MyIcon from '@/components/Icon';
|
import MyIcon from '@/components/Icon';
|
||||||
import InputDataModal from '@/pages/kb/components/InputDataModal';
|
import InputDataModal from '@/pages/kb/detail/components/InputDataModal';
|
||||||
import { getKbDataItemById } from '@/api/plugins/kb';
|
import { getKbDataItemById } from '@/api/plugins/kb';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
import { useLoading } from '@/hooks/useLoading';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
@@ -1,99 +0,0 @@
|
|||||||
import React, { useRef, useState } from 'react';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
|
||||||
import { useToast } from '@/hooks/useToast';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useUserStore } from '@/store/user';
|
|
||||||
import { KbItemType } from '@/types/plugin';
|
|
||||||
import { useScreen } from '@/hooks/useScreen';
|
|
||||||
import { getErrText } from '@/utils/tools';
|
|
||||||
import { type ComponentRef } from './Info';
|
|
||||||
import Tabs from '@/components/Tabs';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import DataCard from './DataCard';
|
|
||||||
|
|
||||||
const Test = dynamic(() => import('./Test'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
const Info = dynamic(() => import('./Info'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
|
|
||||||
enum TabEnum {
|
|
||||||
data = 'data',
|
|
||||||
test = 'test',
|
|
||||||
info = 'info'
|
|
||||||
}
|
|
||||||
|
|
||||||
const Detail = ({ kbId }: { kbId: string }) => {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const router = useRouter();
|
|
||||||
const { isPc } = useScreen();
|
|
||||||
const BasicInfo = useRef<ComponentRef>(null);
|
|
||||||
const { setLastKbId, kbDetail, getKbDetail, loadKbList } = useUserStore();
|
|
||||||
const [currentTab, setCurrentTab] = useState(TabEnum.data);
|
|
||||||
|
|
||||||
const form = useForm<KbItemType>({
|
|
||||||
defaultValues: kbDetail
|
|
||||||
});
|
|
||||||
const { reset } = form;
|
|
||||||
|
|
||||||
useQuery(
|
|
||||||
[kbId],
|
|
||||||
() => {
|
|
||||||
setCurrentTab(TabEnum.data);
|
|
||||||
return getKbDetail(kbId);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess(res) {
|
|
||||||
if (!res) return;
|
|
||||||
kbId && setLastKbId(kbId);
|
|
||||||
reset(res);
|
|
||||||
BasicInfo.current?.initInput?.(res.tags);
|
|
||||||
},
|
|
||||||
onError(err: any) {
|
|
||||||
loadKbList(true);
|
|
||||||
setLastKbId('');
|
|
||||||
router.replace(`/kb`);
|
|
||||||
toast({
|
|
||||||
title: getErrText(err, '获取知识库异常'),
|
|
||||||
status: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
flexDirection={'column'}
|
|
||||||
bg={'#fcfcfc'}
|
|
||||||
h={'100%'}
|
|
||||||
pt={5}
|
|
||||||
overflow={'overlay'}
|
|
||||||
position={'relative'}
|
|
||||||
>
|
|
||||||
<Box mb={3}>
|
|
||||||
<Tabs
|
|
||||||
m={'auto'}
|
|
||||||
w={'260px'}
|
|
||||||
size={isPc ? 'md' : 'sm'}
|
|
||||||
list={[
|
|
||||||
{ id: TabEnum.data, label: '数据管理' },
|
|
||||||
{ id: TabEnum.test, label: '搜索测试' },
|
|
||||||
{ id: TabEnum.info, label: '基本信息' }
|
|
||||||
]}
|
|
||||||
activeId={currentTab}
|
|
||||||
onChange={(e: any) => setCurrentTab(e)}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box flex={'1 0 0'} overflow={'overlay'}>
|
|
||||||
{currentTab === TabEnum.data && <DataCard kbId={kbId} />}
|
|
||||||
{currentTab === TabEnum.test && <Test />}
|
|
||||||
{currentTab === TabEnum.info && <Info ref={BasicInfo} kbId={kbId} form={form} />}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default React.memo(Detail);
|
|
@@ -1,152 +0,0 @@
|
|||||||
import React, { useCallback, useState, useMemo } from 'react';
|
|
||||||
import { Box, Flex, useTheme, Input, IconButton } from '@chakra-ui/react';
|
|
||||||
import { AddIcon } from '@chakra-ui/icons';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { postCreateKb } from '@/api/plugins/kb';
|
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
|
||||||
import { useToast } from '@/hooks/useToast';
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useUserStore } from '@/store/user';
|
|
||||||
import MyIcon from '@/components/Icon';
|
|
||||||
import Avatar from '@/components/Avatar';
|
|
||||||
import Tag from '@/components/Tag';
|
|
||||||
import MyTooltip from '@/components/MyTooltip';
|
|
||||||
|
|
||||||
const KbList = ({ kbId }: { kbId: string }) => {
|
|
||||||
const theme = useTheme();
|
|
||||||
const router = useRouter();
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { Loading, setIsLoading } = useLoading();
|
|
||||||
const { myKbList, loadKbList } = useUserStore();
|
|
||||||
const [searchText, setSearchText] = useState('');
|
|
||||||
|
|
||||||
const kbs = useMemo(
|
|
||||||
() => myKbList.filter((item) => new RegExp(searchText, 'ig').test(item.name + item.tags)),
|
|
||||||
[myKbList, searchText]
|
|
||||||
);
|
|
||||||
|
|
||||||
/* 加载模型 */
|
|
||||||
const { isFetching } = useQuery(['loadModels'], () => loadKbList(false));
|
|
||||||
|
|
||||||
const handleCreateModel = useCallback(async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const name = `知识库${myKbList.length + 1}`;
|
|
||||||
const id = await postCreateKb({ name });
|
|
||||||
await loadKbList(true);
|
|
||||||
toast({
|
|
||||||
title: '创建成功',
|
|
||||||
status: 'success'
|
|
||||||
});
|
|
||||||
router.replace(`/kb?kbId=${id}`);
|
|
||||||
} catch (err: any) {
|
|
||||||
toast({
|
|
||||||
title: typeof err === 'string' ? err : err.message || '出现了意外',
|
|
||||||
status: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
}, [loadKbList, myKbList.length, router, setIsLoading, toast]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex
|
|
||||||
position={'relative'}
|
|
||||||
flexDirection={'column'}
|
|
||||||
w={'100%'}
|
|
||||||
h={'100%'}
|
|
||||||
bg={'white'}
|
|
||||||
borderRight={['', theme.borders.base]}
|
|
||||||
>
|
|
||||||
<Flex w={'90%'} my={5} mx={'auto'}>
|
|
||||||
<Flex flex={1} mr={2} position={'relative'} alignItems={'center'}>
|
|
||||||
<Input
|
|
||||||
h={'32px'}
|
|
||||||
placeholder="搜索知识库"
|
|
||||||
value={searchText}
|
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
|
||||||
/>
|
|
||||||
{searchText && (
|
|
||||||
<MyIcon
|
|
||||||
zIndex={10}
|
|
||||||
position={'absolute'}
|
|
||||||
right={3}
|
|
||||||
name={'closeSolid'}
|
|
||||||
w={'16px'}
|
|
||||||
h={'16px'}
|
|
||||||
color={'myGray.500'}
|
|
||||||
cursor={'pointer'}
|
|
||||||
onClick={() => setSearchText('')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
<MyTooltip label={'新建一个知识库'}>
|
|
||||||
<IconButton
|
|
||||||
h={'32px'}
|
|
||||||
icon={<AddIcon />}
|
|
||||||
aria-label={''}
|
|
||||||
variant={'base'}
|
|
||||||
onClick={handleCreateModel}
|
|
||||||
/>
|
|
||||||
</MyTooltip>
|
|
||||||
</Flex>
|
|
||||||
<Box flex={'1 0 0'} h={0} pl={[0, 2]} overflowY={'scroll'} userSelect={'none'}>
|
|
||||||
{kbs.map((item) => (
|
|
||||||
<Flex
|
|
||||||
key={item._id}
|
|
||||||
position={'relative'}
|
|
||||||
alignItems={['flex-start', 'center']}
|
|
||||||
p={3}
|
|
||||||
mb={[2, 0]}
|
|
||||||
cursor={'pointer'}
|
|
||||||
transition={'background-color .2s ease-in'}
|
|
||||||
borderRadius={['', 'md']}
|
|
||||||
borderBottom={['1px solid #f4f4f4', 'none']}
|
|
||||||
_hover={{
|
|
||||||
backgroundImage: ['', theme.lgColor.hoverBlueGradient]
|
|
||||||
}}
|
|
||||||
{...(kbId === item._id
|
|
||||||
? {
|
|
||||||
backgroundImage: `${theme.lgColor.activeBlueGradient} !important`
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
onClick={() => {
|
|
||||||
if (item._id === kbId) return;
|
|
||||||
router.push(`/kb?kbId=${item._id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar src={item.avatar} w={'34px'} h={'34px'} />
|
|
||||||
<Box flex={'1 0 0'} w={0} ml={3}>
|
|
||||||
<Box className="textEllipsis" color={'myGray.1000'}>
|
|
||||||
{item.name}
|
|
||||||
</Box>
|
|
||||||
{/* tags */}
|
|
||||||
<Box color={'myGray.400'} py={1} fontSize={'sm'}>
|
|
||||||
{!item.tags ? (
|
|
||||||
<>{item.tags || '你还没设置标签~'}</>
|
|
||||||
) : (
|
|
||||||
item.tags.split(' ').map((item, i) => (
|
|
||||||
<Tag key={i} mr={1} mb={1}>
|
|
||||||
{item}
|
|
||||||
</Tag>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!isFetching && myKbList.length === 0 && (
|
|
||||||
<Flex h={'100%'} flexDirection={'column'} alignItems={'center'} pt={'30vh'}>
|
|
||||||
<MyIcon name="empty" w={'48px'} h={'48px'} color={'transparent'} />
|
|
||||||
<Box mt={2} color={'myGray.500'}>
|
|
||||||
知识库空空如也~
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Loading loading={isFetching} fixed={false} />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KbList;
|
|
@@ -31,8 +31,8 @@ import InputModal, { FormData as InputDataType } from './InputDataModal';
|
|||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
|
|
||||||
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
|
const SelectFileModal = dynamic(() => import('./SelectFileModal'), { ssr: true });
|
||||||
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'), { ssr: true });
|
||||||
|
|
||||||
const DataCard = ({ kbId }: { kbId: string }) => {
|
const DataCard = ({ kbId }: { kbId: string }) => {
|
||||||
const lastSearch = useRef('');
|
const lastSearch = useRef('');
|
||||||
@@ -140,7 +140,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
|||||||
}, [kbId]);
|
}, [kbId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position={'relative'} px={5} pb={[1, 5]}>
|
<Box position={'relative'} px={5} py={[1, 5]}>
|
||||||
<Flex justifyContent={'space-between'}>
|
<Flex justifyContent={'space-between'}>
|
||||||
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
|
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
|
||||||
知识库数据: {total}组
|
知识库数据: {total}组
|
@@ -59,7 +59,7 @@ const Info = (
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
router.replace(`/kb?kbId=${myKbList.find((item) => item._id !== kbId)?._id || ''}`);
|
router.replace(`/kb?kbId=${myKbList.find((item) => item._id !== kbId)?._id || ''}`);
|
||||||
await loadKbList(true);
|
await loadKbList();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
title: err?.message || '删除失败',
|
title: err?.message || '删除失败',
|
||||||
@@ -82,7 +82,7 @@ const Info = (
|
|||||||
title: '更新成功',
|
title: '更新成功',
|
||||||
status: 'success'
|
status: 'success'
|
||||||
});
|
});
|
||||||
loadKbList(true);
|
loadKbList();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
toast({
|
toast({
|
||||||
title: err?.message || '更新失败',
|
title: err?.message || '更新失败',
|
||||||
@@ -136,6 +136,7 @@ const Info = (
|
|||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
initInput: (tags: string) => {
|
initInput: (tags: string) => {
|
||||||
|
console.log(tags);
|
||||||
if (InputRef.current) {
|
if (InputRef.current) {
|
||||||
InputRef.current.value = tags;
|
InputRef.current.value = tags;
|
||||||
}
|
}
|
||||||
@@ -143,7 +144,7 @@ const Info = (
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex px={5} flexDirection={'column'} alignItems={'center'}>
|
<Flex p={5} flexDirection={'column'} alignItems={'center'}>
|
||||||
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'center'}>
|
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'center'}>
|
||||||
<Box flex={'0 0 90px'} w={0}>
|
<Box flex={'0 0 90px'} w={0}>
|
||||||
知识库头像
|
知识库头像
|
||||||
@@ -200,31 +201,29 @@ const Info = (
|
|||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
{kbDetail._id && (
|
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'flex-end'}>
|
||||||
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'flex-end'}>
|
<Box flex={'0 0 90px'} w={0}></Box>
|
||||||
<Box flex={'0 0 90px'} w={0}></Box>
|
<Button
|
||||||
<Button
|
isLoading={btnLoading}
|
||||||
isLoading={btnLoading}
|
mr={4}
|
||||||
mr={4}
|
w={'100px'}
|
||||||
w={'100px'}
|
onClick={handleSubmit(saveSubmitSuccess, saveSubmitError)}
|
||||||
onClick={handleSubmit(saveSubmitSuccess, saveSubmitError)}
|
>
|
||||||
>
|
保存
|
||||||
保存
|
</Button>
|
||||||
</Button>
|
<IconButton
|
||||||
<IconButton
|
isLoading={btnLoading}
|
||||||
isLoading={btnLoading}
|
icon={<DeleteIcon />}
|
||||||
icon={<DeleteIcon />}
|
aria-label={''}
|
||||||
aria-label={''}
|
variant={'outline'}
|
||||||
variant={'outline'}
|
size={'sm'}
|
||||||
size={'sm'}
|
_hover={{
|
||||||
_hover={{
|
color: 'red.600',
|
||||||
color: 'red.600',
|
borderColor: 'red.600'
|
||||||
borderColor: 'red.600'
|
}}
|
||||||
}}
|
onClick={openConfirm(onclickDelKb)}
|
||||||
onClick={openConfirm(onclickDelKb)}
|
/>
|
||||||
/>
|
</Flex>
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
<File onSelect={onSelectFile} />
|
<File onSelect={onSelectFile} />
|
||||||
<ConfirmChild />
|
<ConfirmChild />
|
||||||
</Flex>
|
</Flex>
|
@@ -11,7 +11,10 @@ import InputDataModal, { type FormData } from './InputDataModal';
|
|||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import { vectorModelList } from '@/store/static';
|
||||||
import { customAlphabet } from 'nanoid';
|
import { customAlphabet } from 'nanoid';
|
||||||
|
import MyTooltip from '@/components/MyTooltip';
|
||||||
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
|
||||||
|
|
||||||
const Test = () => {
|
const Test = () => {
|
||||||
@@ -30,7 +33,7 @@ const Test = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { mutate, isLoading } = useRequest({
|
const { mutate, isLoading } = useRequest({
|
||||||
mutationFn: () => searchText({ kbId, text: inputText.trim() }),
|
mutationFn: () => searchText({ model: vectorModelList[0].model, kbId, text: inputText.trim() }),
|
||||||
onSuccess(res) {
|
onSuccess(res) {
|
||||||
const testItem = {
|
const testItem = {
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
@@ -40,7 +43,6 @@ const Test = () => {
|
|||||||
results: res
|
results: res
|
||||||
};
|
};
|
||||||
pushKbTestItem(testItem);
|
pushKbTestItem(testItem);
|
||||||
setInputText('');
|
|
||||||
setKbTestItem(testItem);
|
setKbTestItem(testItem);
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
@@ -59,13 +61,14 @@ const Test = () => {
|
|||||||
<Box h={'100%'} display={['block', 'flex']}>
|
<Box h={'100%'} display={['block', 'flex']}>
|
||||||
<Box
|
<Box
|
||||||
h={['auto', '100%']}
|
h={['auto', '100%']}
|
||||||
overflow={'overlay'}
|
display={['block', 'flex']}
|
||||||
|
flexDirection={'column'}
|
||||||
flex={1}
|
flex={1}
|
||||||
maxW={'500px'}
|
maxW={'500px'}
|
||||||
px={4}
|
py={4}
|
||||||
borderRight={['none', theme.borders.base]}
|
borderRight={['none', theme.borders.base]}
|
||||||
>
|
>
|
||||||
<Box border={'2px solid'} borderColor={'myBlue.600'} p={3} borderRadius={'md'}>
|
<Box border={'2px solid'} borderColor={'myBlue.600'} p={3} mx={4} borderRadius={'md'}>
|
||||||
<Box fontSize={'sm'} fontWeight={'bold'}>
|
<Box fontSize={'sm'} fontWeight={'bold'}>
|
||||||
<MyIcon mr={2} name={'text'} w={'18px'} h={'18px'} color={'myBlue.700'} />
|
<MyIcon mr={2} name={'text'} w={'18px'} h={'18px'} color={'myBlue.700'} />
|
||||||
测试文本
|
测试文本
|
||||||
@@ -85,13 +88,13 @@ const Test = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Box>
|
||||||
<Box mt={5} display={['none', 'block']}>
|
<Box mt={5} flex={'1 0 0'} px={4} overflow={'overlay'} display={['none', 'block']}>
|
||||||
<Flex alignItems={'center'} color={'myGray.600'}>
|
<Flex alignItems={'center'} color={'myGray.600'}>
|
||||||
<MyIcon mr={2} name={'history'} w={'16px'} h={'16px'} />
|
<MyIcon mr={2} name={'history'} w={'16px'} h={'16px'} />
|
||||||
<Box fontSize={'2xl'}>测试历史</Box>
|
<Box fontSize={'2xl'}>测试历史</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box mt={2}>
|
<Box mt={2}>
|
||||||
<Flex py={1} fontWeight={'bold'} borderBottom={theme.borders.base}>
|
<Flex py={2} fontWeight={'bold'} borderBottom={theme.borders.sm}>
|
||||||
<Box flex={1}>测试文本</Box>
|
<Box flex={1}>测试文本</Box>
|
||||||
<Box w={'80px'}>时间</Box>
|
<Box w={'80px'}>时间</Box>
|
||||||
<Box w={'14px'}></Box>
|
<Box w={'14px'}></Box>
|
||||||
@@ -115,26 +118,28 @@ const Test = () => {
|
|||||||
{item.text}
|
{item.text}
|
||||||
</Box>
|
</Box>
|
||||||
<Box w={'80px'}>{formatTimeToChatTime(item.time)}</Box>
|
<Box w={'80px'}>{formatTimeToChatTime(item.time)}</Box>
|
||||||
<Box w={'14px'} h={'14px'}>
|
<MyTooltip label={'删除该测试记录'}>
|
||||||
<MyIcon
|
<Box w={'14px'} h={'14px'}>
|
||||||
className="delete"
|
<MyIcon
|
||||||
name={'delete'}
|
className="delete"
|
||||||
w={'14px'}
|
name={'delete'}
|
||||||
display={'none'}
|
w={'14px'}
|
||||||
_hover={{ color: 'red.600' }}
|
display={'none'}
|
||||||
onClick={(e) => {
|
_hover={{ color: 'red.600' }}
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
delKbTestItemById(item.id);
|
e.stopPropagation();
|
||||||
kbTestItem?.id === item.id && setKbTestItem(undefined);
|
delKbTestItemById(item.id);
|
||||||
}}
|
kbTestItem?.id === item.id && setKbTestItem(undefined);
|
||||||
/>
|
}}
|
||||||
</Box>
|
/>
|
||||||
|
</Box>
|
||||||
|
</MyTooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box px={4} pb={4} mt={[8, 0]} h={['auto', '100%']} overflow={'overlay'} flex={1}>
|
<Box p={4} h={['auto', '100%']} overflow={'overlay'} flex={1}>
|
||||||
{!kbTestItem?.results || kbTestItem.results.length === 0 ? (
|
{!kbTestItem?.results || kbTestItem.results.length === 0 ? (
|
||||||
<Flex
|
<Flex
|
||||||
mt={[10, 0]}
|
mt={[10, 0]}
|
||||||
@@ -154,9 +159,18 @@ const Test = () => {
|
|||||||
<Box fontSize={'3xl'} color={'myGray.600'}>
|
<Box fontSize={'3xl'} color={'myGray.600'}>
|
||||||
测试结果
|
测试结果
|
||||||
</Box>
|
</Box>
|
||||||
<Box fontSize={'xs'} color={'myGray.500'} ml={1}>
|
<MyTooltip
|
||||||
QA内容可能不是最新
|
label={
|
||||||
</Box>
|
'根据知识库内容与测试文本的相似度进行排序,你可以根据测试结果调整对应的文本。\n注意:测试记录中的数据可能已经被修改过,点击某条测试数据后将展示最新的数据。'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<QuestionOutlineIcon
|
||||||
|
ml={2}
|
||||||
|
color={'myGray.600'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
fontSize={'lg'}
|
||||||
|
/>
|
||||||
|
</MyTooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Grid
|
<Grid
|
||||||
mt={1}
|
mt={1}
|
160
client/src/pages/kb/detail/index.tsx
Normal file
160
client/src/pages/kb/detail/index.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import React, { useCallback, useMemo, useRef } from 'react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
|
import { KbItemType } from '@/types/plugin';
|
||||||
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
|
import { getErrText } from '@/utils/tools';
|
||||||
|
import { type ComponentRef } from './components/Info';
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import DataCard from './components/DataCard';
|
||||||
|
import MyIcon from '@/components/Icon';
|
||||||
|
import SideTabs from '@/components/SideTabs';
|
||||||
|
import PageContainer from '@/components/PageContainer';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
import Info from './components/Info';
|
||||||
|
const Test = dynamic(() => import('./components/Test'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
|
||||||
|
enum TabEnum {
|
||||||
|
data = 'data',
|
||||||
|
test = 'test',
|
||||||
|
info = 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
const Detail = ({ kbId, currentTab }: { kbId: string; currentTab: `${TabEnum}` }) => {
|
||||||
|
const InfoRef = useRef<ComponentRef>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const router = useRouter();
|
||||||
|
const { isPc } = useScreen();
|
||||||
|
const { kbDetail, getKbDetail } = useUserStore();
|
||||||
|
|
||||||
|
const tabList = useMemo(
|
||||||
|
() => [
|
||||||
|
{ label: '数据集', id: TabEnum.data, icon: 'overviewLight' },
|
||||||
|
{ label: '搜索测试', id: TabEnum.test, icon: 'kbTest' },
|
||||||
|
{ label: '基本信息', id: TabEnum.info, icon: 'settingLight' }
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const setCurrentTab = useCallback(
|
||||||
|
(tab: `${TabEnum}`) => {
|
||||||
|
router.replace({
|
||||||
|
query: {
|
||||||
|
kbId,
|
||||||
|
currentTab: tab
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[kbId, router]
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<KbItemType>({
|
||||||
|
defaultValues: kbDetail
|
||||||
|
});
|
||||||
|
|
||||||
|
useQuery([kbId], () => getKbDetail(kbId), {
|
||||||
|
onSuccess(res) {
|
||||||
|
InfoRef.current?.initInput(res.tags);
|
||||||
|
form.reset(res);
|
||||||
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
router.replace(`/kb/list`);
|
||||||
|
toast({
|
||||||
|
title: getErrText(err, '获取知识库异常'),
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Box display={['block', 'flex']} h={'100%'} pt={[4, 0]}>
|
||||||
|
{/* pc tab */}
|
||||||
|
<Box
|
||||||
|
display={['none', 'flex']}
|
||||||
|
flexDirection={'column'}
|
||||||
|
p={4}
|
||||||
|
w={'200px'}
|
||||||
|
borderRight={theme.borders.base}
|
||||||
|
>
|
||||||
|
<Flex mb={4} alignItems={'center'}>
|
||||||
|
<Avatar src={kbDetail.avatar} w={'34px'} borderRadius={'lg'} />
|
||||||
|
<Box ml={2} fontWeight={'bold'}>
|
||||||
|
{kbDetail.name}
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<SideTabs
|
||||||
|
flex={1}
|
||||||
|
mx={'auto'}
|
||||||
|
mt={2}
|
||||||
|
w={'100%'}
|
||||||
|
list={tabList}
|
||||||
|
activeId={currentTab}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setCurrentTab(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Flex
|
||||||
|
alignItems={'center'}
|
||||||
|
cursor={'pointer'}
|
||||||
|
py={2}
|
||||||
|
px={3}
|
||||||
|
borderRadius={'md'}
|
||||||
|
_hover={{ bg: 'myGray.100' }}
|
||||||
|
onClick={() => router.replace('/kb/list')}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
mr={3}
|
||||||
|
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
|
||||||
|
bg={'white'}
|
||||||
|
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||||
|
h={'28px'}
|
||||||
|
size={'sm'}
|
||||||
|
borderRadius={'50%'}
|
||||||
|
aria-label={''}
|
||||||
|
/>
|
||||||
|
全部知识库
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
<Box mb={3} display={['block', 'none']}>
|
||||||
|
<Tabs
|
||||||
|
m={'auto'}
|
||||||
|
w={'260px'}
|
||||||
|
size={isPc ? 'md' : 'sm'}
|
||||||
|
list={[
|
||||||
|
{ id: TabEnum.data, label: '数据管理' },
|
||||||
|
{ id: TabEnum.test, label: '搜索测试' },
|
||||||
|
{ id: TabEnum.info, label: '基本信息' }
|
||||||
|
]}
|
||||||
|
activeId={currentTab}
|
||||||
|
onChange={(e: any) => setCurrentTab(e)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box flex={'1 0 0'} overflow={'overlay'} pb={[4, 0]}>
|
||||||
|
{currentTab === TabEnum.data && <DataCard kbId={kbId} />}
|
||||||
|
{currentTab === TabEnum.test && <Test />}
|
||||||
|
{currentTab === TabEnum.info && <Info ref={InfoRef} kbId={kbId} form={form} />}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: any) {
|
||||||
|
const currentTab = context?.query?.currentTab || TabEnum.data;
|
||||||
|
const kbId = context?.query?.kbId;
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { currentTab, kbId }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(Detail);
|
@@ -1,40 +0,0 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { Box, Flex } from '@chakra-ui/react';
|
|
||||||
import { useGlobalStore } from '@/store/global';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import { useUserStore } from '@/store/user';
|
|
||||||
import SideBar from '@/components/SideBar';
|
|
||||||
import KbList from './components/KbList';
|
|
||||||
import KbDetail from './components/Detail';
|
|
||||||
|
|
||||||
const Kb = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
const { kbId = '' } = router.query as { kbId: string };
|
|
||||||
const { isPc } = useGlobalStore();
|
|
||||||
const { lastKbId } = useUserStore();
|
|
||||||
|
|
||||||
// redirect
|
|
||||||
useEffect(() => {
|
|
||||||
if (isPc && !kbId && lastKbId) {
|
|
||||||
router.replace(`/kb?kbId=${lastKbId}`);
|
|
||||||
}
|
|
||||||
}, [isPc, kbId, lastKbId, router]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex h={'100%'} position={'relative'} overflow={'hidden'}>
|
|
||||||
{/* 模型列表 */}
|
|
||||||
{(isPc || !kbId) && (
|
|
||||||
<SideBar w={['100%', '0 0 250px', '0 0 270px', '0 0 290px']}>
|
|
||||||
<KbList kbId={kbId} />
|
|
||||||
</SideBar>
|
|
||||||
)}
|
|
||||||
{!!kbId && (
|
|
||||||
<Box flex={'1 0 0'} w={0} h={'100%'} position={'relative'}>
|
|
||||||
<KbDetail kbId={kbId} />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Kb;
|
|
149
client/src/pages/kb/list/index.tsx
Normal file
149
client/src/pages/kb/list/index.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { Box, Card, Flex, Grid, useTheme, Button, IconButton } from '@chakra-ui/react';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useUserStore } from '@/store/user';
|
||||||
|
import PageContainer from '@/components/PageContainer';
|
||||||
|
import { useConfirm } from '@/hooks/useConfirm';
|
||||||
|
import { AddIcon } from '@chakra-ui/icons';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useToast } from '@/hooks/useToast';
|
||||||
|
import { delKbById, postCreateKb } from '@/api/plugins/kb';
|
||||||
|
import { useRequest } from '@/hooks/useRequest';
|
||||||
|
import Avatar from '@/components/Avatar';
|
||||||
|
import MyIcon from '@/components/Icon';
|
||||||
|
import Tag from '@/components/Tag';
|
||||||
|
|
||||||
|
const Kb = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const router = useRouter();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { openConfirm, ConfirmChild } = useConfirm({
|
||||||
|
title: '删除提示',
|
||||||
|
content: '确认删除该知识库?'
|
||||||
|
});
|
||||||
|
const { myKbList, loadKbList, setKbList } = useUserStore();
|
||||||
|
|
||||||
|
useQuery(['loadKbList'], () => loadKbList());
|
||||||
|
|
||||||
|
/* 点击删除 */
|
||||||
|
const onclickDelKb = useCallback(
|
||||||
|
async (id: string) => {
|
||||||
|
try {
|
||||||
|
delKbById(id);
|
||||||
|
toast({
|
||||||
|
title: '删除成功',
|
||||||
|
status: 'success'
|
||||||
|
});
|
||||||
|
setKbList(myKbList.filter((item) => item._id !== id));
|
||||||
|
} catch (err: any) {
|
||||||
|
toast({
|
||||||
|
title: err?.message || '删除失败',
|
||||||
|
status: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toast, setKbList, myKbList]
|
||||||
|
);
|
||||||
|
|
||||||
|
/* create a new kb and router to it */
|
||||||
|
const { mutate: onclickCreate, isLoading } = useRequest({
|
||||||
|
mutationFn: async () => {
|
||||||
|
const name = `知识库${myKbList.length + 1}`;
|
||||||
|
const id = await postCreateKb({ name });
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
successToast: '创建成功',
|
||||||
|
errorToast: '创建知识库出现意外',
|
||||||
|
onSuccess(id) {
|
||||||
|
router.push(`/kb/detail?kbId=${id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<Flex pt={3} px={5} alignItems={'center'}>
|
||||||
|
<Box flex={1} className="textlg" letterSpacing={1} fontSize={'24px'} fontWeight={'bold'}>
|
||||||
|
我的知识库
|
||||||
|
</Box>
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
leftIcon={<AddIcon />}
|
||||||
|
variant={'base'}
|
||||||
|
onClick={onclickCreate}
|
||||||
|
>
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<Grid
|
||||||
|
p={5}
|
||||||
|
gridTemplateColumns={['1fr', 'repeat(3,1fr)', 'repeat(4,1fr)', 'repeat(5,1fr)']}
|
||||||
|
gridGap={5}
|
||||||
|
>
|
||||||
|
{myKbList.map((kb) => (
|
||||||
|
<Card
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection={'column'}
|
||||||
|
key={kb._id}
|
||||||
|
py={4}
|
||||||
|
px={5}
|
||||||
|
cursor={'pointer'}
|
||||||
|
h={'140px'}
|
||||||
|
border={theme.borders.md}
|
||||||
|
boxShadow={'none'}
|
||||||
|
userSelect={'none'}
|
||||||
|
position={'relative'}
|
||||||
|
_hover={{
|
||||||
|
boxShadow: '1px 1px 10px rgba(0,0,0,0.2)',
|
||||||
|
borderColor: 'transparent',
|
||||||
|
'& .delete': {
|
||||||
|
display: 'block'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
router.push({
|
||||||
|
pathname: '/kb/detail',
|
||||||
|
query: {
|
||||||
|
kbId: kb._id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Flex alignItems={'center'} h={'38px'}>
|
||||||
|
<Avatar src={kb.avatar} borderRadius={'lg'} w={'28px'} />
|
||||||
|
<Box ml={3}>{kb.name}</Box>
|
||||||
|
<IconButton
|
||||||
|
className="delete"
|
||||||
|
position={'absolute'}
|
||||||
|
top={4}
|
||||||
|
right={4}
|
||||||
|
size={'sm'}
|
||||||
|
icon={<MyIcon name={'delete'} w={'14px'} />}
|
||||||
|
variant={'base'}
|
||||||
|
borderRadius={'md'}
|
||||||
|
aria-label={'delete'}
|
||||||
|
display={['', 'none']}
|
||||||
|
_hover={{
|
||||||
|
bg: 'red.100'
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
openConfirm(() => onclickDelKb(kb._id))();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Box flex={'1 0 0'} overflow={'hidden'} pt={2}>
|
||||||
|
{kb.tags.map((tag, i) => (
|
||||||
|
<Tag key={i} mr={2} mb={2}>
|
||||||
|
{tag}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
<ConfirmChild />
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Kb;
|
@@ -7,7 +7,7 @@ import { formatPrice } from '@/utils/user';
|
|||||||
import { getTokenLogin } from '@/api/user';
|
import { getTokenLogin } from '@/api/user';
|
||||||
import { defaultApp } from '@/constants/model';
|
import { defaultApp } from '@/constants/model';
|
||||||
import { AppListItemType } from '@/types/app';
|
import { AppListItemType } from '@/types/app';
|
||||||
import { KbItemType } from '@/types/plugin';
|
import type { KbItemType, KbListItemType } from '@/types/plugin';
|
||||||
import { getKbList, getKbById } from '@/api/plugins/kb';
|
import { getKbList, getKbById } from '@/api/plugins/kb';
|
||||||
import { defaultKbDetail } from '@/constants/kb';
|
import { defaultKbDetail } from '@/constants/kb';
|
||||||
import type { AppSchema } from '@/types/mongoSchema';
|
import type { AppSchema } from '@/types/mongoSchema';
|
||||||
@@ -23,10 +23,9 @@ type State = {
|
|||||||
appDetail: AppSchema;
|
appDetail: AppSchema;
|
||||||
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
|
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
|
||||||
// kb
|
// kb
|
||||||
lastKbId: string;
|
myKbList: KbListItemType[];
|
||||||
setLastKbId: (id: string) => void;
|
loadKbList: () => Promise<any>;
|
||||||
myKbList: KbItemType[];
|
setKbList(val: KbListItemType[]): void;
|
||||||
loadKbList: (init?: boolean) => Promise<KbItemType[]>;
|
|
||||||
kbDetail: KbItemType;
|
kbDetail: KbItemType;
|
||||||
getKbDetail: (id: string, init?: boolean) => Promise<KbItemType>;
|
getKbDetail: (id: string, init?: boolean) => Promise<KbItemType>;
|
||||||
};
|
};
|
||||||
@@ -79,21 +78,19 @@ export const useUserStore = create<State>()(
|
|||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
lastKbId: '',
|
|
||||||
setLastKbId(id: string) {
|
|
||||||
set((state) => {
|
|
||||||
state.lastKbId = id;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
myKbList: [],
|
myKbList: [],
|
||||||
async loadKbList(init = false) {
|
async loadKbList() {
|
||||||
if (get().myKbList.length > 0 && !init) return get().myKbList;
|
|
||||||
const res = await getKbList();
|
const res = await getKbList();
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.myKbList = res;
|
state.myKbList = res;
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
setKbList(val: KbListItemType[]) {
|
||||||
|
set((state) => {
|
||||||
|
state.myKbList = val;
|
||||||
|
});
|
||||||
|
},
|
||||||
kbDetail: defaultKbDetail,
|
kbDetail: defaultKbDetail,
|
||||||
async getKbDetail(id: string, init = false) {
|
async getKbDetail(id: string, init = false) {
|
||||||
if (id === get().kbDetail._id && !init) return get().kbDetail;
|
if (id === get().kbDetail._id && !init) return get().kbDetail;
|
||||||
@@ -109,9 +106,7 @@ export const useUserStore = create<State>()(
|
|||||||
})),
|
})),
|
||||||
{
|
{
|
||||||
name: 'userStore',
|
name: 'userStore',
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({})
|
||||||
lastKbId: state.lastKbId
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
24
client/src/styles/default.scss
Normal file
24
client/src/styles/default.scss
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#nprogress .bar {
|
||||||
|
background: '#1237b3' !important; //自定义颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
.textEllipsis {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.textEllipsis3 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.grecaptcha-badge {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.textlg {
|
||||||
|
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
@@ -1,3 +1,6 @@
|
|||||||
.react-flow__panel {
|
.react-flow__panel {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.react-flow__panel.react-flow__attribution {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
@@ -1,3 +1,6 @@
|
|||||||
|
@import './reactflow.scss';
|
||||||
|
@import './default.scss';
|
||||||
|
|
||||||
body,
|
body,
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
@@ -71,26 +74,6 @@ textarea::placeholder {
|
|||||||
#__next {
|
#__next {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
#nprogress .bar {
|
|
||||||
background: '#1237b3' !important; //自定义颜色
|
|
||||||
}
|
|
||||||
|
|
||||||
.textEllipsis {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.grecaptcha-badge {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.react-flow__panel.react-flow__attribution {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
.textlg {
|
|
||||||
background: linear-gradient(to bottom right, #1237b3 0%, #3370ff 40%, #4e83fd 80%, #85b1ff 100%);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
html {
|
html {
|
||||||
|
6
client/src/types/plugin.d.ts
vendored
6
client/src/types/plugin.d.ts
vendored
@@ -1,5 +1,11 @@
|
|||||||
import type { kbSchema } from './mongoSchema';
|
import type { kbSchema } from './mongoSchema';
|
||||||
|
|
||||||
|
export type KbListItemType = {
|
||||||
|
_id: string;
|
||||||
|
avatar: string;
|
||||||
|
name: string;
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
/* kb type */
|
/* kb type */
|
||||||
export interface KbItemType extends kbSchema {
|
export interface KbItemType extends kbSchema {
|
||||||
totalData: number;
|
totalData: number;
|
||||||
|
Reference in New Issue
Block a user