mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-23 13:03:50 +00:00
feat: kb ui
This commit is contained in:
@@ -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数据
|
||||
|
@@ -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: '知识库',
|
||||
icon: 'dbLight',
|
||||
activeIcon: 'dbFill',
|
||||
link: `/kb`,
|
||||
activeLink: ['/kb']
|
||||
link: `/kb/list`,
|
||||
activeLink: ['/kb/list', '/kb/detail']
|
||||
},
|
||||
{
|
||||
label: '市场',
|
||||
|
@@ -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;`
|
||||
);
|
||||
|
@@ -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 {
|
||||
|
@@ -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(' ')
|
||||
}
|
||||
});
|
||||
|
@@ -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) {
|
||||
|
@@ -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';
|
||||
|
@@ -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 { 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}组
|
@@ -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>
|
@@ -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}
|
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 { 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) => ({})
|
||||
}
|
||||
)
|
||||
)
|
||||
|
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 {
|
||||
display: none;
|
||||
}
|
||||
.react-flow__panel.react-flow__attribution {
|
||||
display: none !important;
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
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';
|
||||
|
||||
export type KbListItemType = {
|
||||
_id: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
tags: string[];
|
||||
};
|
||||
/* kb type */
|
||||
export interface KbItemType extends kbSchema {
|
||||
totalData: number;
|
||||
|
Reference in New Issue
Block a user