feat: kb ui

This commit is contained in:
archer
2023-07-14 11:49:36 +08:00
parent 358c4716f9
commit 5a96e167ee
24 changed files with 463 additions and 412 deletions

View File

@@ -1,5 +1,5 @@
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 { TrainingModeEnum } from '@/constants/plugin';
import {
@@ -10,6 +10,7 @@ import {
Props as SearchTestProps,
Response as SearchTestResponse
} from '@/pages/api/openapi/kb/searchTest';
import { Response as KbDataItemType } from '@/pages/api/plugins/kb/data/getDataById';
export type KbUpdateParams = {
id: string;
@@ -19,7 +20,7 @@ export type KbUpdateParams = {
};
/* 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}`);
@@ -59,7 +60,7 @@ export const getTrainingData = (data: { kbId: string; init: boolean }) =>
}>(`/plugins/kb/data/getTrainingData`, data);
export const getKbDataItemById = (dataId: string) =>
GET(`/plugins/kb/data/getDataById`, { dataId });
GET<KbDataItemType>(`/plugins/kb/data/getDataById`, { dataId });
/**
* 直接push数据

View File

@@ -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

View File

@@ -38,8 +38,8 @@ const Navbar = ({ unread }: { unread: number }) => {
label: '知识库',
icon: 'dbLight',
activeIcon: 'dbFill',
link: `/kb`,
activeLink: ['/kb']
link: `/kb/list`,
activeLink: ['/kb/list', '/kb/detail']
},
{
label: '市场',

View File

@@ -7,6 +7,7 @@ import { getVector } from '../plugin/vector';
import type { KbTestItemType } from '@/types/plugin';
export type Props = {
model: string;
kbId: string;
text: string;
};
@@ -14,9 +15,9 @@ export type Response = KbTestItemType['results'];
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
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('缺少参数');
}
@@ -27,7 +28,8 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
throw new Error('缺少用户ID');
}
const vector = await getVector({
const { vectors } = await getVector({
model,
userId,
input: [text]
});
@@ -36,9 +38,9 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
`BEGIN;
SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10};
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 <#> '[${
vector[0]
vectors[0]
}]' limit 12;
COMMIT;`
);

View File

@@ -5,6 +5,13 @@ import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
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>) {
try {
let { dataId } = req.query as {

View File

@@ -18,10 +18,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const data = await KB.findOne({
_id: id,
userId
});
const data = await KB.findOne(
{
_id: id,
userId
},
'_id avatar name userId tags'
);
if (!data) {
throw new Error('kb is not exist');
@@ -33,7 +36,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
avatar: data.avatar,
name: data.name,
userId: data.userId,
updateTime: data.updateTime,
tags: data.tags.join(' ')
}
});

View File

@@ -2,8 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, KB } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import { KbItemType } from '@/types/plugin';
import { KbListItemType } from '@/types/plugin';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
@@ -12,25 +11,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
await connectToDatabase();
const kbList = await KB.find({
userId
}).sort({ updateTime: -1 });
const kbList = await KB.find(
{
userId
},
'_id avatar name tags'
).sort({ updateTime: -1 });
const data = await Promise.all(
kbList.map(async (item) => ({
_id: item._id,
avatar: item.avatar,
name: item.name,
userId: item.userId,
updateTime: item.updateTime,
tags: item.tags.join(' '),
totalData: await PgClient.count('modelData', {
where: [['user_id', userId], 'AND', ['kb_id', item._id]]
})
tags: item.tags
}))
);
jsonRes<KbItemType[]>(res, {
jsonRes<KbListItemType[]>(res, {
data
});
} catch (err) {

View File

@@ -10,7 +10,7 @@ import {
useTheme
} from '@chakra-ui/react';
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 { useLoading } from '@/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';

View File

@@ -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);

View File

@@ -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;

View File

@@ -31,8 +31,8 @@ import InputModal, { FormData as InputDataType } from './InputDataModal';
import { debounce } from 'lodash';
import { getErrText } from '@/utils/tools';
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
const SelectFileModal = dynamic(() => import('./SelectFileModal'), { ssr: true });
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'), { ssr: true });
const DataCard = ({ kbId }: { kbId: string }) => {
const lastSearch = useRef('');
@@ -140,7 +140,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
}, [kbId]);
return (
<Box position={'relative'} px={5} pb={[1, 5]}>
<Box position={'relative'} px={5} py={[1, 5]}>
<Flex justifyContent={'space-between'}>
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
: {total}

View File

@@ -59,7 +59,7 @@ const Info = (
status: 'success'
});
router.replace(`/kb?kbId=${myKbList.find((item) => item._id !== kbId)?._id || ''}`);
await loadKbList(true);
await loadKbList();
} catch (err: any) {
toast({
title: err?.message || '删除失败',
@@ -82,7 +82,7 @@ const Info = (
title: '更新成功',
status: 'success'
});
loadKbList(true);
loadKbList();
} catch (err: any) {
toast({
title: err?.message || '更新失败',
@@ -136,6 +136,7 @@ const Info = (
useImperativeHandle(ref, () => ({
initInput: (tags: string) => {
console.log(tags);
if (InputRef.current) {
InputRef.current.value = tags;
}
@@ -143,7 +144,7 @@ const Info = (
}));
return (
<Flex px={5} flexDirection={'column'} alignItems={'center'}>
<Flex p={5} flexDirection={'column'} alignItems={'center'}>
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'center'}>
<Box flex={'0 0 90px'} w={0}>
@@ -200,31 +201,29 @@ const Info = (
))}
</Box>
</Flex>
{kbDetail._id && (
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'flex-end'}>
<Box flex={'0 0 90px'} w={0}></Box>
<Button
isLoading={btnLoading}
mr={4}
w={'100px'}
onClick={handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
<IconButton
isLoading={btnLoading}
icon={<DeleteIcon />}
aria-label={''}
variant={'outline'}
size={'sm'}
_hover={{
color: 'red.600',
borderColor: 'red.600'
}}
onClick={openConfirm(onclickDelKb)}
/>
</Flex>
)}
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'flex-end'}>
<Box flex={'0 0 90px'} w={0}></Box>
<Button
isLoading={btnLoading}
mr={4}
w={'100px'}
onClick={handleSubmit(saveSubmitSuccess, saveSubmitError)}
>
</Button>
<IconButton
isLoading={btnLoading}
icon={<DeleteIcon />}
aria-label={''}
variant={'outline'}
size={'sm'}
_hover={{
color: 'red.600',
borderColor: 'red.600'
}}
onClick={openConfirm(onclickDelKb)}
/>
</Flex>
<File onSelect={onSelectFile} />
<ConfirmChild />
</Flex>

View File

@@ -11,7 +11,10 @@ import InputDataModal, { type FormData } from './InputDataModal';
import { useGlobalStore } from '@/store/global';
import { getErrText } from '@/utils/tools';
import { useToast } from '@/hooks/useToast';
import { vectorModelList } from '@/store/static';
import { customAlphabet } from 'nanoid';
import MyTooltip from '@/components/MyTooltip';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 12);
const Test = () => {
@@ -30,7 +33,7 @@ const Test = () => {
);
const { mutate, isLoading } = useRequest({
mutationFn: () => searchText({ kbId, text: inputText.trim() }),
mutationFn: () => searchText({ model: vectorModelList[0].model, kbId, text: inputText.trim() }),
onSuccess(res) {
const testItem = {
id: nanoid(),
@@ -40,7 +43,6 @@ const Test = () => {
results: res
};
pushKbTestItem(testItem);
setInputText('');
setKbTestItem(testItem);
},
onError(err) {
@@ -59,13 +61,14 @@ const Test = () => {
<Box h={'100%'} display={['block', 'flex']}>
<Box
h={['auto', '100%']}
overflow={'overlay'}
display={['block', 'flex']}
flexDirection={'column'}
flex={1}
maxW={'500px'}
px={4}
py={4}
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'}>
<MyIcon mr={2} name={'text'} w={'18px'} h={'18px'} color={'myBlue.700'} />
@@ -85,13 +88,13 @@ const Test = () => {
</Button>
</Flex>
</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'}>
<MyIcon mr={2} name={'history'} w={'16px'} h={'16px'} />
<Box fontSize={'2xl'}></Box>
</Flex>
<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 w={'80px'}></Box>
<Box w={'14px'}></Box>
@@ -115,26 +118,28 @@ const Test = () => {
{item.text}
</Box>
<Box w={'80px'}>{formatTimeToChatTime(item.time)}</Box>
<Box w={'14px'} h={'14px'}>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
display={'none'}
_hover={{ color: 'red.600' }}
onClick={(e) => {
e.stopPropagation();
delKbTestItemById(item.id);
kbTestItem?.id === item.id && setKbTestItem(undefined);
}}
/>
</Box>
<MyTooltip label={'删除该测试记录'}>
<Box w={'14px'} h={'14px'}>
<MyIcon
className="delete"
name={'delete'}
w={'14px'}
display={'none'}
_hover={{ color: 'red.600' }}
onClick={(e) => {
e.stopPropagation();
delKbTestItemById(item.id);
kbTestItem?.id === item.id && setKbTestItem(undefined);
}}
/>
</Box>
</MyTooltip>
</Flex>
))}
</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 ? (
<Flex
mt={[10, 0]}
@@ -154,9 +159,18 @@ const Test = () => {
<Box fontSize={'3xl'} color={'myGray.600'}>
</Box>
<Box fontSize={'xs'} color={'myGray.500'} ml={1}>
QA内容可能不是最新
</Box>
<MyTooltip
label={
'根据知识库内容与测试文本的相似度进行排序,你可以根据测试结果调整对应的文本。\n注意测试记录中的数据可能已经被修改过点击某条测试数据后将展示最新的数据。'
}
>
<QuestionOutlineIcon
ml={2}
color={'myGray.600'}
cursor={'pointer'}
fontSize={'lg'}
/>
</MyTooltip>
</Flex>
<Grid
mt={1}

View 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);

View File

@@ -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;

View 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;

View File

@@ -7,7 +7,7 @@ import { formatPrice } from '@/utils/user';
import { getTokenLogin } from '@/api/user';
import { defaultApp } from '@/constants/model';
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 { defaultKbDetail } from '@/constants/kb';
import type { AppSchema } from '@/types/mongoSchema';
@@ -23,10 +23,9 @@ type State = {
appDetail: AppSchema;
loadAppDetail: (id: string, init?: boolean) => Promise<AppSchema>;
// kb
lastKbId: string;
setLastKbId: (id: string) => void;
myKbList: KbItemType[];
loadKbList: (init?: boolean) => Promise<KbItemType[]>;
myKbList: KbListItemType[];
loadKbList: () => Promise<any>;
setKbList(val: KbListItemType[]): void;
kbDetail: KbItemType;
getKbDetail: (id: string, init?: boolean) => Promise<KbItemType>;
};
@@ -79,21 +78,19 @@ export const useUserStore = create<State>()(
});
return res;
},
lastKbId: '',
setLastKbId(id: string) {
set((state) => {
state.lastKbId = id;
});
},
myKbList: [],
async loadKbList(init = false) {
if (get().myKbList.length > 0 && !init) return get().myKbList;
async loadKbList() {
const res = await getKbList();
set((state) => {
state.myKbList = res;
});
return res;
},
setKbList(val: KbListItemType[]) {
set((state) => {
state.myKbList = val;
});
},
kbDetail: defaultKbDetail,
async getKbDetail(id: string, init = false) {
if (id === get().kbDetail._id && !init) return get().kbDetail;
@@ -109,9 +106,7 @@ export const useUserStore = create<State>()(
})),
{
name: 'userStore',
partialize: (state) => ({
lastKbId: state.lastKbId
})
partialize: (state) => ({})
}
)
)

View 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;
}

View File

@@ -1,3 +1,6 @@
.react-flow__panel {
display: none;
}
.react-flow__panel.react-flow__attribution {
display: none !important;
}

View File

@@ -1,3 +1,6 @@
@import './reactflow.scss';
@import './default.scss';
body,
h1,
h2,
@@ -71,26 +74,6 @@ textarea::placeholder {
#__next {
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) {
html {

View File

@@ -1,5 +1,11 @@
import type { kbSchema } from './mongoSchema';
export type KbListItemType = {
_id: string;
avatar: string;
name: string;
tags: string[];
};
/* kb type */
export interface KbItemType extends kbSchema {
totalData: number;