mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-20 02:34:52 +00:00
feat: kb UI
This commit is contained in:
@@ -37,8 +37,8 @@ Fast GPT 允许你使用自己的 openai API KEY 来快速的调用 openai 接
|
|||||||
## 👀 其他
|
## 👀 其他
|
||||||
|
|
||||||
- [FastGpt 常见问题](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
- [FastGpt 常见问题](https://kjqvjse66l.feishu.cn/docx/HtrgdT0pkonP4kxGx8qcu6XDnGh)
|
||||||
|
- [docker 部署教程](https://www.bilibili.com/video/BV1jo4y147fT/)
|
||||||
- [公众号接入](https://www.bilibili.com/video/BV1xh4y1t7fy/)
|
- [公众号接入](https://www.bilibili.com/video/BV1xh4y1t7fy/)
|
||||||
- [FastGpt + Laf 最佳实践,将知识库装入公众号,点击去 Laf 公众号体验效果](https://b4jky7-fastgpt.oss.laf.run/lafercode.png)
|
|
||||||
- [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4)
|
- [FastGpt V3.4 更新集合](https://www.bilibili.com/video/BV1Lo4y147Qh/?vd_source=92041a1a395f852f9d89158eaa3f61b4)
|
||||||
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
- [FastGpt 知识库演示](https://www.bilibili.com/video/BV1Wo4y1p7i1/)
|
||||||
|
|
||||||
|
@@ -29,11 +29,11 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
|
|||||||
}, [colorSchema]);
|
}, [colorSchema]);
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={'span'}
|
display={'inline-block'}
|
||||||
border={'1px solid'}
|
border={'1px solid'}
|
||||||
px={2}
|
px={2}
|
||||||
lineHeight={0}
|
lineHeight={1}
|
||||||
py={'1px'}
|
py={'2px'}
|
||||||
borderRadius={'md'}
|
borderRadius={'md'}
|
||||||
fontSize={'xs'}
|
fontSize={'xs'}
|
||||||
{...theme}
|
{...theme}
|
||||||
|
@@ -249,8 +249,8 @@ export const theme = extendTheme({
|
|||||||
'2xl': '2100px'
|
'2xl': '2100px'
|
||||||
},
|
},
|
||||||
lgColor: {
|
lgColor: {
|
||||||
activeBlueGradient: 'linear-gradient(120deg, #d6e8ff 0%, #f0f7ff 100%)',
|
activeBlueGradient: 'linear-gradient(to bottom right, #d6e8ff 0%, #f0f7ff 100%)',
|
||||||
hoverBlueGradient: 'linear-gradient(60deg, #f0f7ff 0%, #d6e8ff 100%)',
|
hoverBlueGradient: 'linear-gradient(to top left, #d6e8ff 0%, #f0f7ff 100%)',
|
||||||
primary: 'linear-gradient(to bottom right, #2152d9 0%,#3370ff 40%, #4e83fd 100%)',
|
primary: 'linear-gradient(to bottom right, #2152d9 0%,#3370ff 40%, #4e83fd 100%)',
|
||||||
primary2: 'linear-gradient(to bottom right, #2152d9 0%,#3370ff 30%,#4e83fd 80%, #85b1ff 100%)'
|
primary2: 'linear-gradient(to bottom right, #2152d9 0%,#3370ff 30%,#4e83fd 80%, #85b1ff 100%)'
|
||||||
},
|
},
|
||||||
|
@@ -54,6 +54,7 @@ export const usePagination = <T = any,>({
|
|||||||
size={'sm'}
|
size={'sm'}
|
||||||
w={'28px'}
|
w={'28px'}
|
||||||
h={'28px'}
|
h={'28px'}
|
||||||
|
isLoading={isLoading}
|
||||||
onClick={() => mutate(pageNum - 1)}
|
onClick={() => mutate(pageNum - 1)}
|
||||||
/>
|
/>
|
||||||
<Flex mx={2} alignItems={'center'}>
|
<Flex mx={2} alignItems={'center'}>
|
||||||
@@ -84,13 +85,14 @@ export const usePagination = <T = any,>({
|
|||||||
icon={<ArrowForwardIcon />}
|
icon={<ArrowForwardIcon />}
|
||||||
aria-label={'left'}
|
aria-label={'left'}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
|
isLoading={isLoading}
|
||||||
w={'28px'}
|
w={'28px'}
|
||||||
h={'28px'}
|
h={'28px'}
|
||||||
onClick={() => mutate(pageNum + 1)}
|
onClick={() => mutate(pageNum + 1)}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}, [maxPage, mutate, pageNum]);
|
}, [isLoading, maxPage, mutate, pageNum]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
defaultRequest && mutate(1);
|
defaultRequest && mutate(1);
|
||||||
|
@@ -26,6 +26,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
const { userId } = await authUser({ req, authToken: true });
|
const { userId } = await authUser({ req, authToken: true });
|
||||||
|
|
||||||
await connectToDatabase();
|
await connectToDatabase();
|
||||||
|
searchText = searchText.replace(/'/g, '');
|
||||||
|
|
||||||
const where: any = [
|
const where: any = [
|
||||||
['user_id', userId],
|
['user_id', userId],
|
||||||
|
@@ -1,13 +1,7 @@
|
|||||||
import React, { useCallback, useState, useRef, useEffect } from 'react';
|
import React, { useCallback, useState, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
TableContainer,
|
Card,
|
||||||
Table,
|
|
||||||
Thead,
|
|
||||||
Tbody,
|
|
||||||
Tr,
|
|
||||||
Th,
|
|
||||||
Td,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Flex,
|
Flex,
|
||||||
Button,
|
Button,
|
||||||
@@ -17,10 +11,8 @@ import {
|
|||||||
MenuList,
|
MenuList,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Input,
|
Input,
|
||||||
Tooltip
|
Grid
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
|
||||||
import type { BoxProps } from '@chakra-ui/react';
|
|
||||||
import type { KbDataItemType } from '@/types/plugin';
|
import type { KbDataItemType } from '@/types/plugin';
|
||||||
import { usePagination } from '@/hooks/usePagination';
|
import { usePagination } from '@/hooks/usePagination';
|
||||||
import {
|
import {
|
||||||
@@ -29,30 +21,21 @@ import {
|
|||||||
delOneKbDataByDataId,
|
delOneKbDataByDataId,
|
||||||
getTrainingData
|
getTrainingData
|
||||||
} from '@/api/plugins/kb';
|
} from '@/api/plugins/kb';
|
||||||
import { DeleteIcon, RepeatIcon, EditIcon } from '@chakra-ui/icons';
|
import { DeleteIcon } from '@chakra-ui/icons';
|
||||||
import { useLoading } from '@/hooks/useLoading';
|
|
||||||
import { fileDownload } from '@/utils/file';
|
import { fileDownload } from '@/utils/file';
|
||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import InputModal, { FormData as InputDataType } from './InputDataModal';
|
import InputModal, { FormData as InputDataType } from './InputDataModal';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
|
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
|
||||||
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
||||||
|
|
||||||
const DataCard = ({ kbId }: { kbId: string }) => {
|
const DataCard = ({ kbId }: { kbId: string }) => {
|
||||||
const lastSearch = useRef('');
|
const lastSearch = useRef('');
|
||||||
const tdStyles = useRef<BoxProps>({
|
|
||||||
fontSize: 'xs',
|
|
||||||
minW: '150px',
|
|
||||||
maxW: '500px',
|
|
||||||
maxH: '250px',
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
overflowY: 'auto'
|
|
||||||
});
|
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const { Loading, setIsLoading } = useLoading();
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -64,7 +47,7 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
|||||||
pageNum
|
pageNum
|
||||||
} = usePagination<KbDataItemType>({
|
} = usePagination<KbDataItemType>({
|
||||||
api: getKbDataList,
|
api: getKbDataList,
|
||||||
pageSize: 10,
|
pageSize: 24,
|
||||||
params: {
|
params: {
|
||||||
kbId,
|
kbId,
|
||||||
searchText
|
searchText
|
||||||
@@ -109,7 +92,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
|||||||
mutationFn: () => getExportDataList(kbId),
|
mutationFn: () => getExportDataList(kbId),
|
||||||
onSuccess(res) {
|
onSuccess(res) {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
|
||||||
const text = Papa.unparse({
|
const text = Papa.unparse({
|
||||||
fields: ['question', 'answer'],
|
fields: ['question', 'answer'],
|
||||||
data: res
|
data: res
|
||||||
@@ -126,7 +108,6 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
error;
|
error;
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
|
||||||
},
|
},
|
||||||
onError(err: any) {
|
onError(err: any) {
|
||||||
toast({
|
toast({
|
||||||
@@ -137,6 +118,14 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getFirstData = useCallback(
|
||||||
|
debounce(() => {
|
||||||
|
getData(1);
|
||||||
|
lastSearch.current = searchText;
|
||||||
|
}, 300),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
// interval get data
|
// interval get data
|
||||||
useQuery(['refetchData'], () => refetchData(1), {
|
useQuery(['refetchData'], () => refetchData(1), {
|
||||||
refetchInterval: 5000,
|
refetchInterval: 5000,
|
||||||
@@ -150,148 +139,137 @@ const DataCard = ({ kbId }: { kbId: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box position={'relative'}>
|
<Box position={'relative'}>
|
||||||
<Flex>
|
<Flex justifyContent={'space-between'}>
|
||||||
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
|
<Box fontWeight={'bold'} fontSize={'lg'} mr={2}>
|
||||||
知识库数据: {total}组
|
知识库数据: {total}组
|
||||||
</Box>
|
</Box>
|
||||||
<IconButton
|
<Box>
|
||||||
icon={<RepeatIcon />}
|
<Button
|
||||||
aria-label={'refresh'}
|
variant={'base'}
|
||||||
variant={'base'}
|
mr={2}
|
||||||
mr={[2, 4]}
|
size={'sm'}
|
||||||
size={'sm'}
|
isLoading={isLoadingExport || isLoading}
|
||||||
onClick={() => {
|
title={'半小时仅能导出1次'}
|
||||||
refetchData(pageNum);
|
onClick={() => onclickExport()}
|
||||||
getTrainingData({ kbId, init: true });
|
>
|
||||||
}}
|
导出csv
|
||||||
/>
|
</Button>
|
||||||
<Button
|
<Menu autoSelect={false}>
|
||||||
variant={'base'}
|
<MenuButton as={Button} size={'sm'} isLoading={isLoading}>
|
||||||
mr={2}
|
导入
|
||||||
size={'sm'}
|
</MenuButton>
|
||||||
isLoading={isLoadingExport}
|
<MenuList>
|
||||||
title={'半小时仅能导出1次'}
|
<MenuItem
|
||||||
onClick={() => onclickExport()}
|
onClick={() =>
|
||||||
>
|
setEditInputData({
|
||||||
导出csv
|
a: '',
|
||||||
</Button>
|
q: ''
|
||||||
<Menu autoSelect={false}>
|
})
|
||||||
<MenuButton as={Button} size={'sm'}>
|
}
|
||||||
导入
|
>
|
||||||
</MenuButton>
|
手动输入
|
||||||
<MenuList>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem onClick={onOpenSelectFileModal}>文本/文件拆分</MenuItem>
|
||||||
onClick={() =>
|
<MenuItem onClick={onOpenSelectCsvModal}>csv 问答对导入</MenuItem>
|
||||||
setEditInputData({
|
</MenuList>
|
||||||
a: '',
|
</Menu>
|
||||||
q: ''
|
</Box>
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
手动输入
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={onOpenSelectFileModal}>文本/文件拆分</MenuItem>
|
|
||||||
<MenuItem onClick={onOpenSelectCsvModal}>csv 问答对导入</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex mt={4}>
|
<Flex my={4}>
|
||||||
{(qaListLen > 0 || vectorListLen > 0) && (
|
{qaListLen > 0 || vectorListLen > 0 ? (
|
||||||
<Box fontSize={'xs'}>
|
<Box fontSize={'xs'}>
|
||||||
{qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
{qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
||||||
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
||||||
请耐心等待...
|
请耐心等待...
|
||||||
</Box>
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Box fontSize={'xs'}>所有数据已就绪~</Box>
|
||||||
)}
|
)}
|
||||||
<Box flex={1} />
|
<Box flex={1} mr={1} />
|
||||||
<Input
|
<Input
|
||||||
maxW={['90%', '300px']}
|
maxW={['60%', '300px']}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
placeholder="搜索匹配知识,补充知识和来源,回车确认"
|
placeholder="根据匹配知识,补充知识和来源搜索"
|
||||||
onChange={(e) => setSearchText(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearchText(e.target.value);
|
||||||
|
getFirstData();
|
||||||
|
}}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
if (searchText === lastSearch.current) return;
|
if (searchText === lastSearch.current) return;
|
||||||
getData(1);
|
getFirstData();
|
||||||
lastSearch.current = searchText;
|
|
||||||
}}
|
}}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (searchText === lastSearch.current) return;
|
if (searchText === lastSearch.current) return;
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
getData(1);
|
getFirstData();
|
||||||
lastSearch.current = searchText;
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<TableContainer mt={4} minH={'200px'}>
|
<Grid
|
||||||
<Table>
|
minH={'200px'}
|
||||||
<Thead>
|
gridTemplateColumns={['1fr', 'repeat(2,1fr)', 'repeat(3,1fr)', 'repeat(4,1fr)']}
|
||||||
<Tr>
|
gridGap={4}
|
||||||
<Th>
|
>
|
||||||
匹配的知识点
|
{kbDataList.map((item) => (
|
||||||
<Tooltip
|
<Card
|
||||||
label={
|
key={item.id}
|
||||||
'对话时,会将用户的问题和知识库的 "匹配知识点" 进行比较,找到最相似的前 n 条记录,将这些记录的 "匹配知识点"+"补充知识点" 作为 chatgpt 的系统提示词。'
|
cursor={'pointer'}
|
||||||
}
|
pb={3}
|
||||||
>
|
userSelect={'none'}
|
||||||
<QuestionOutlineIcon ml={1} />
|
boxShadow={'none'}
|
||||||
</Tooltip>
|
_hover={{ boxShadow: 'lg', '& .delete': { display: 'block' } }}
|
||||||
</Th>
|
border={'1px solid '}
|
||||||
<Th>补充知识</Th>
|
borderColor={'myGray.200'}
|
||||||
<Th>来源</Th>
|
onClick={() =>
|
||||||
<Th>操作</Th>
|
setEditInputData({
|
||||||
</Tr>
|
dataId: item.id,
|
||||||
</Thead>
|
q: item.q,
|
||||||
<Tbody>
|
a: item.a
|
||||||
{kbDataList.map((item) => (
|
})
|
||||||
<Tr key={item.id} fontSize={'sm'}>
|
}
|
||||||
<Td>
|
>
|
||||||
<Box {...tdStyles.current}>{item.q}</Box>
|
<Flex py={3} px={4} h={'36px'}>
|
||||||
</Td>
|
<Box className={'textEllipsis'} flex={1} fontSize={'sm'}>
|
||||||
<Td>
|
{item.source?.trim()}
|
||||||
<Box {...tdStyles.current}>{item.a || '-'}</Box>
|
</Box>
|
||||||
</Td>
|
<IconButton
|
||||||
<Td maxW={'15%'} whiteSpace={'pre-wrap'} userSelect={'all'}>
|
className="delete"
|
||||||
{item.source?.trim() || '-'}
|
display={['block', 'none']}
|
||||||
</Td>
|
icon={<DeleteIcon />}
|
||||||
<Td>
|
variant={'base'}
|
||||||
<IconButton
|
colorScheme={'gray'}
|
||||||
mr={5}
|
aria-label={'delete'}
|
||||||
icon={<EditIcon />}
|
size={'xs'}
|
||||||
variant={'base'}
|
borderRadius={'md'}
|
||||||
aria-label={'delete'}
|
lineHeight={1}
|
||||||
size={'sm'}
|
_hover={{ color: 'red.600' }}
|
||||||
onClick={() =>
|
onClick={async (e) => {
|
||||||
setEditInputData({
|
e.stopPropagation();
|
||||||
dataId: item.id,
|
await delOneKbDataByDataId(item.id);
|
||||||
q: item.q,
|
refetchData(pageNum);
|
||||||
a: item.a
|
}}
|
||||||
})
|
/>
|
||||||
}
|
</Flex>
|
||||||
/>
|
<Box
|
||||||
<IconButton
|
h={'100px'}
|
||||||
icon={<DeleteIcon />}
|
overflow={'hidden'}
|
||||||
variant={'base'}
|
wordBreak={'break-all'}
|
||||||
colorScheme={'gray'}
|
px={3}
|
||||||
aria-label={'delete'}
|
color={'myGray.600'}
|
||||||
size={'sm'}
|
>
|
||||||
onClick={async () => {
|
<Box color={'myGray.1000'}>{item.q}</Box>
|
||||||
await delOneKbDataByDataId(item.id);
|
<Box color={'myGray.600'}>{item.a}</Box>
|
||||||
refetchData(pageNum);
|
</Box>
|
||||||
}}
|
</Card>
|
||||||
/>
|
))}
|
||||||
</Td>
|
</Grid>
|
||||||
</Tr>
|
|
||||||
))}
|
<Flex mt={2} justifyContent={'center'}>
|
||||||
</Tbody>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
|
||||||
<Flex mt={2} justifyContent={'flex-end'}>
|
|
||||||
<Pagination />
|
<Pagination />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<Loading loading={isLoading} fixed={false} />
|
|
||||||
{editInputData !== undefined && (
|
{editInputData !== undefined && (
|
||||||
<InputModal
|
<InputModal
|
||||||
kbId={kbId}
|
kbId={kbId}
|
||||||
|
@@ -1,21 +1,30 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Card, Box } from '@chakra-ui/react';
|
import { Box } from '@chakra-ui/react';
|
||||||
import { useToast } from '@/hooks/useToast';
|
import { useToast } from '@/hooks/useToast';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useUserStore } from '@/store/user';
|
import { useUserStore } from '@/store/user';
|
||||||
import { KbItemType } from '@/types/plugin';
|
import { KbItemType } from '@/types/plugin';
|
||||||
|
import { useScreen } from '@/hooks/useScreen';
|
||||||
import DataCard from './DataCard';
|
import DataCard from './DataCard';
|
||||||
import { getErrText } from '@/utils/tools';
|
import { getErrText } from '@/utils/tools';
|
||||||
import Info, { type ComponentRef } from './Info';
|
import Info, { type ComponentRef } from './Info';
|
||||||
|
import Tabs from '@/components/Tabs';
|
||||||
|
|
||||||
|
enum TabEnum {
|
||||||
|
data = 'data',
|
||||||
|
test = 'test',
|
||||||
|
info = 'info'
|
||||||
|
}
|
||||||
|
|
||||||
const Detail = ({ kbId }: { kbId: string }) => {
|
const Detail = ({ kbId }: { kbId: string }) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isPc } = useScreen();
|
||||||
const BasicInfo = useRef<ComponentRef>(null);
|
const BasicInfo = useRef<ComponentRef>(null);
|
||||||
const { setLastKbId, kbDetail, getKbDetail, loadKbList, myKbList } = useUserStore();
|
const { setLastKbId, kbDetail, getKbDetail, loadKbList, myKbList } = useUserStore();
|
||||||
|
const [currentTab, setCurrentTab] = useState(TabEnum.data);
|
||||||
|
|
||||||
const form = useForm<KbItemType>({
|
const form = useForm<KbItemType>({
|
||||||
defaultValues: kbDetail
|
defaultValues: kbDetail
|
||||||
@@ -42,13 +51,23 @@ const Detail = ({ kbId }: { kbId: string }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box h={'100%'} p={5} overflow={'overlay'} position={'relative'}>
|
<Box bg={'#fcfcfc'} h={'100%'} p={5} overflow={'overlay'} position={'relative'}>
|
||||||
<Card p={6}>
|
<Box mb={5}>
|
||||||
<Info ref={BasicInfo} kbId={kbId} form={form} />
|
<Tabs
|
||||||
</Card>
|
m={'auto'}
|
||||||
<Card p={6} mt={5}>
|
w={'260px'}
|
||||||
<DataCard kbId={kbId} />
|
size={isPc ? 'md' : 'sm'}
|
||||||
</Card>
|
list={[
|
||||||
|
{ id: TabEnum.data, label: '数据管理' },
|
||||||
|
{ id: TabEnum.test, label: '搜索测试' },
|
||||||
|
{ id: TabEnum.info, label: '基本信息' }
|
||||||
|
]}
|
||||||
|
activeId={currentTab}
|
||||||
|
onChange={(e: any) => setCurrentTab(e)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
{currentTab === TabEnum.data && <DataCard kbId={kbId} />}
|
||||||
|
{currentTab === TabEnum.info && <Info ref={BasicInfo} kbId={kbId} form={form} />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -140,88 +140,89 @@ const Info = (
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Flex flexDirection={'column'} alignItems={'center'}>
|
||||||
<Flex>
|
<Flex mt={5} w={'100%'} maxW={'350px'} alignItems={'center'}>
|
||||||
<Box fontWeight={'bold'} fontSize={'2xl'} flex={1}>
|
<Box flex={'0 0 90px'} w={0}>
|
||||||
知识库信息
|
知识库头像
|
||||||
|
</Box>
|
||||||
|
<Box flex={1}>
|
||||||
|
<Avatar
|
||||||
|
m={'auto'}
|
||||||
|
src={getValues('avatar')}
|
||||||
|
w={['32px', '40px']}
|
||||||
|
h={['32px', '40px']}
|
||||||
|
cursor={'pointer'}
|
||||||
|
title={'点击切换头像'}
|
||||||
|
onClick={onOpenSelectFile}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{kbDetail._id && (
|
|
||||||
<>
|
|
||||||
<Button
|
|
||||||
isLoading={btnLoading}
|
|
||||||
mr={3}
|
|
||||||
onClick={handleSubmit(saveSubmitSuccess, saveSubmitError)}
|
|
||||||
>
|
|
||||||
保存
|
|
||||||
</Button>
|
|
||||||
<IconButton
|
|
||||||
isLoading={btnLoading}
|
|
||||||
icon={<DeleteIcon />}
|
|
||||||
aria-label={''}
|
|
||||||
variant={'solid'}
|
|
||||||
colorScheme={'red'}
|
|
||||||
onClick={openConfirm(onclickDelKb)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex mt={5} alignItems={'center'}>
|
<FormControl mt={8} w={'100%'} maxW={'350px'} display={'flex'} alignItems={'center'}>
|
||||||
<Box flex={'0 0 60px'} w={0}>
|
<Box flex={'0 0 90px'} w={0}>
|
||||||
头像
|
知识库名称
|
||||||
</Box>
|
</Box>
|
||||||
<Avatar
|
<Input
|
||||||
src={getValues('avatar')}
|
flex={1}
|
||||||
w={['28px', '36px']}
|
{...register('name', {
|
||||||
h={['28px', '36px']}
|
required: '知识库名称不能为空'
|
||||||
cursor={'pointer'}
|
})}
|
||||||
title={'点击切换头像'}
|
|
||||||
onClick={onOpenSelectFile}
|
|
||||||
/>
|
/>
|
||||||
</Flex>
|
|
||||||
<FormControl mt={5}>
|
|
||||||
<Flex alignItems={'center'} maxW={'350px'}>
|
|
||||||
<Box flex={'0 0 60px'} w={0}>
|
|
||||||
名称
|
|
||||||
</Box>
|
|
||||||
<Input
|
|
||||||
{...register('name', {
|
|
||||||
required: '知识库名称不能为空'
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Box>
|
<Flex mt={8} alignItems={'center'} w={'100%'} maxW={'350px'} flexWrap={'wrap'}>
|
||||||
<Flex mt={5} alignItems={'center'} maxW={'350px'} flexWrap={'wrap'}>
|
<Box flex={'0 0 90px'} w={0}>
|
||||||
<Box flex={'0 0 60px'} w={0}>
|
分类标签
|
||||||
标签
|
<Tooltip label={'用空格隔开多个标签,便于搜索'}>
|
||||||
<Tooltip label={'仅用于记忆,用空格隔开多个标签'}>
|
<QuestionOutlineIcon ml={1} />
|
||||||
<QuestionOutlineIcon ml={1} />
|
</Tooltip>
|
||||||
</Tooltip>
|
</Box>
|
||||||
</Box>
|
<Input
|
||||||
<Input
|
flex={1}
|
||||||
flex={1}
|
maxW={'300px'}
|
||||||
ref={InputRef}
|
ref={InputRef}
|
||||||
placeholder={'标签,使用空格分割。'}
|
placeholder={'标签,使用空格分割。'}
|
||||||
onChange={(e) => {
|
maxLength={30}
|
||||||
setValue('tags', e.target.value);
|
onChange={(e) => {
|
||||||
setRefresh(!refresh);
|
setValue('tags', e.target.value);
|
||||||
}}
|
setRefresh(!refresh);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box mt={2} w="100%">
|
||||||
|
{getValues('tags')
|
||||||
|
.split(' ')
|
||||||
|
.filter((item) => item)
|
||||||
|
.map((item, i) => (
|
||||||
|
<Tag mr={2} mb={2} key={i} whiteSpace={'nowrap'}>
|
||||||
|
{item}
|
||||||
|
</Tag>
|
||||||
|
))}
|
||||||
|
</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'}
|
||||||
|
colorScheme={'red'}
|
||||||
|
color={'red.500'}
|
||||||
|
onClick={openConfirm(onclickDelKb)}
|
||||||
/>
|
/>
|
||||||
<Box pl={'60px'} mt={2} w="100%">
|
|
||||||
{getValues('tags')
|
|
||||||
.split(' ')
|
|
||||||
.filter((item) => item)
|
|
||||||
.map((item, i) => (
|
|
||||||
<Tag mr={2} mb={2} key={i}>
|
|
||||||
{item}
|
|
||||||
</Tag>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
)}
|
||||||
<File onSelect={onSelectFile} />
|
<File onSelect={onSelectFile} />
|
||||||
<ConfirmChild />
|
<ConfirmChild />
|
||||||
</Box>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -119,12 +119,12 @@ const KbList = ({ kbId }: { kbId: string }) => {
|
|||||||
{item.name}
|
{item.name}
|
||||||
</Box>
|
</Box>
|
||||||
{/* tags */}
|
{/* tags */}
|
||||||
<Box className="textEllipsis" color={'myGray.400'} py={1}>
|
<Box color={'myGray.400'} py={1} fontSize={'sm'}>
|
||||||
{!item.tags ? (
|
{!item.tags ? (
|
||||||
<>{item.tags || '你还没设置标签~'}</>
|
<>{item.tags || '你还没设置标签~'}</>
|
||||||
) : (
|
) : (
|
||||||
item.tags.split(' ').map((item, i) => (
|
item.tags.split(' ').map((item, i) => (
|
||||||
<Tag key={i} mr={2} mb={2}>
|
<Tag key={i} mr={1} mb={1}>
|
||||||
{item}
|
{item}
|
||||||
</Tag>
|
</Tag>
|
||||||
))
|
))
|
||||||
|
@@ -245,13 +245,14 @@ const NumberSetting = ({ tableType }: { tableType: `${TableEnum}` }) => {
|
|||||||
|
|
||||||
<Card mt={4} px={[3, 6]} py={4}>
|
<Card mt={4} px={[3, 6]} py={4}>
|
||||||
<Tabs
|
<Tabs
|
||||||
|
m={'auto'}
|
||||||
w={'200px'}
|
w={'200px'}
|
||||||
list={tableList.current}
|
list={tableList.current}
|
||||||
activeId={currentTab}
|
activeId={currentTab}
|
||||||
size={'sm'}
|
size={'sm'}
|
||||||
onChange={(id: any) => setCurrentTab(id)}
|
onChange={(id: any) => setCurrentTab(id)}
|
||||||
/>
|
/>
|
||||||
<Box>
|
<Box minH={'300px'}>
|
||||||
{(() => {
|
{(() => {
|
||||||
const item = tableList.current.find((item) => item.id === currentTab);
|
const item = tableList.current.find((item) => item.id === currentTab);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user