310 lines
8.4 KiB
TypeScript
310 lines
8.4 KiB
TypeScript
import React, { useCallback, useState, useRef } from 'react';
|
|
import {
|
|
Box,
|
|
TableContainer,
|
|
Table,
|
|
Thead,
|
|
Tbody,
|
|
Tr,
|
|
Th,
|
|
Td,
|
|
IconButton,
|
|
Flex,
|
|
Button,
|
|
useDisclosure,
|
|
Menu,
|
|
MenuButton,
|
|
MenuList,
|
|
MenuItem,
|
|
Input,
|
|
Tooltip
|
|
} from '@chakra-ui/react';
|
|
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
|
import type { BoxProps } from '@chakra-ui/react';
|
|
import type { KbDataItemType } from '@/types/plugin';
|
|
import { usePagination } from '@/hooks/usePagination';
|
|
import {
|
|
getKbDataList,
|
|
getExportDataList,
|
|
delOneKbDataByDataId,
|
|
getTrainingData
|
|
} from '@/api/plugins/kb';
|
|
import { DeleteIcon, RepeatIcon, EditIcon } from '@chakra-ui/icons';
|
|
import { useLoading } from '@/hooks/useLoading';
|
|
import { fileDownload } from '@/utils/file';
|
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
import { useToast } from '@/hooks/useToast';
|
|
import Papa from 'papaparse';
|
|
import dynamic from 'next/dynamic';
|
|
import InputModal, { FormData as InputDataType } from './InputDataModal';
|
|
|
|
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
|
|
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
|
|
|
const DataCard = ({ kbId }: { kbId: string }) => {
|
|
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 { Loading, setIsLoading } = useLoading();
|
|
const { toast } = useToast();
|
|
|
|
const {
|
|
data: modelDataList,
|
|
isLoading,
|
|
Pagination,
|
|
total,
|
|
getData,
|
|
pageNum
|
|
} = usePagination<KbDataItemType>({
|
|
api: getKbDataList,
|
|
pageSize: 10,
|
|
params: {
|
|
kbId,
|
|
searchText
|
|
},
|
|
defaultRequest: false
|
|
});
|
|
|
|
useQuery(['getKbData', kbId], () => {
|
|
getData(1);
|
|
return null;
|
|
});
|
|
|
|
const [editInputData, setEditInputData] = useState<InputDataType>();
|
|
|
|
const {
|
|
isOpen: isOpenSelectFileModal,
|
|
onOpen: onOpenSelectFileModal,
|
|
onClose: onCloseSelectFileModal
|
|
} = useDisclosure();
|
|
const {
|
|
isOpen: isOpenSelectCsvModal,
|
|
onOpen: onOpenSelectCsvModal,
|
|
onClose: onCloseSelectCsvModal
|
|
} = useDisclosure();
|
|
|
|
const { data: { qaListLen = 0, vectorListLen = 0 } = {}, refetch } = useQuery(
|
|
['getModelSplitDataList', kbId],
|
|
() => getTrainingData({ kbId, init: false }),
|
|
{
|
|
onError(err) {
|
|
console.log(err);
|
|
}
|
|
}
|
|
);
|
|
|
|
const refetchData = useCallback(
|
|
(num = 1) => {
|
|
getData(num);
|
|
refetch();
|
|
return null;
|
|
},
|
|
[getData, refetch]
|
|
);
|
|
|
|
// interval get data
|
|
useQuery(['refetchData'], () => refetchData(pageNum), {
|
|
refetchInterval: 5000,
|
|
enabled: qaListLen > 0 || vectorListLen > 0
|
|
});
|
|
|
|
// get al data and export csv
|
|
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
|
|
mutationFn: () => getExportDataList(kbId),
|
|
onSuccess(res) {
|
|
try {
|
|
setIsLoading(true);
|
|
const text = Papa.unparse({
|
|
fields: ['question', 'answer'],
|
|
data: res
|
|
});
|
|
fileDownload({
|
|
text,
|
|
type: 'text/csv',
|
|
filename: 'data.csv'
|
|
});
|
|
toast({
|
|
title: '导出成功,下次导出需要半小时后',
|
|
status: 'success'
|
|
});
|
|
} catch (error) {
|
|
error;
|
|
}
|
|
setIsLoading(false);
|
|
},
|
|
onError(err: any) {
|
|
toast({
|
|
title: typeof err === 'string' ? err : err?.message || '导出异常',
|
|
status: 'error'
|
|
});
|
|
console.log(err);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<Box position={'relative'}>
|
|
<Flex>
|
|
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
|
|
知识库数据: {total}组
|
|
</Box>
|
|
<IconButton
|
|
icon={<RepeatIcon />}
|
|
aria-label={'refresh'}
|
|
variant={'outline'}
|
|
mr={[2, 4]}
|
|
size={'sm'}
|
|
onClick={() => {
|
|
refetchData(pageNum);
|
|
getTrainingData({ kbId, init: true });
|
|
}}
|
|
/>
|
|
<Button
|
|
variant={'outline'}
|
|
mr={2}
|
|
size={'sm'}
|
|
isLoading={isLoadingExport}
|
|
title={'半小时仅能导出1次'}
|
|
onClick={() => onclickExport()}
|
|
>
|
|
导出csv
|
|
</Button>
|
|
<Menu autoSelect={false}>
|
|
<MenuButton as={Button} size={'sm'}>
|
|
导入
|
|
</MenuButton>
|
|
<MenuList>
|
|
<MenuItem
|
|
onClick={() =>
|
|
setEditInputData({
|
|
a: '',
|
|
q: ''
|
|
})
|
|
}
|
|
>
|
|
手动输入
|
|
</MenuItem>
|
|
<MenuItem onClick={onOpenSelectFileModal}>文本/文件拆分</MenuItem>
|
|
<MenuItem onClick={onOpenSelectCsvModal}>csv 问答对导入</MenuItem>
|
|
</MenuList>
|
|
</Menu>
|
|
</Flex>
|
|
<Flex mt={4}>
|
|
{(qaListLen > 0 || vectorListLen > 0) && (
|
|
<Box fontSize={'xs'}>
|
|
{qaListLen > 0 ? `${qaListLen}条数据正在拆分,` : ''}
|
|
{vectorListLen > 0 ? `${vectorListLen}条数据正在生成索引,` : ''}
|
|
请耐心等待...
|
|
</Box>
|
|
)}
|
|
<Box flex={1} />
|
|
<Input
|
|
maxW={'240px'}
|
|
size={'sm'}
|
|
value={searchText}
|
|
placeholder="搜索相关问题和答案,回车确认"
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
onBlur={() => {
|
|
if (searchText === lastSearch.current) return;
|
|
getData(1);
|
|
lastSearch.current = searchText;
|
|
}}
|
|
onKeyDown={(e) => {
|
|
if (searchText === lastSearch.current) return;
|
|
if (e.key === 'Enter') {
|
|
getData(1);
|
|
lastSearch.current = searchText;
|
|
}
|
|
}}
|
|
/>
|
|
</Flex>
|
|
<TableContainer mt={4} minH={'200px'}>
|
|
<Table>
|
|
<Thead>
|
|
<Tr>
|
|
<Th>
|
|
匹配的知识点
|
|
<Tooltip
|
|
label={
|
|
'对话时,会将用户的问题和知识库的 "匹配知识点" 进行比较,找到最相似的前 n 条记录,将这些记录的 "匹配知识点"+"补充知识点" 作为 chatgpt 的系统提示词。'
|
|
}
|
|
>
|
|
<QuestionOutlineIcon ml={1} />
|
|
</Tooltip>
|
|
</Th>
|
|
<Th>补充知识</Th>
|
|
<Th>操作</Th>
|
|
</Tr>
|
|
</Thead>
|
|
<Tbody>
|
|
{modelDataList.map((item) => (
|
|
<Tr key={item.id}>
|
|
<Td>
|
|
<Box {...tdStyles.current}>{item.q}</Box>
|
|
</Td>
|
|
<Td>
|
|
<Box {...tdStyles.current}>{item.a || '-'}</Box>
|
|
</Td>
|
|
<Td>
|
|
<IconButton
|
|
mr={5}
|
|
icon={<EditIcon />}
|
|
variant={'outline'}
|
|
aria-label={'delete'}
|
|
size={'sm'}
|
|
onClick={() =>
|
|
setEditInputData({
|
|
dataId: item.id,
|
|
q: item.q,
|
|
a: item.a
|
|
})
|
|
}
|
|
/>
|
|
<IconButton
|
|
icon={<DeleteIcon />}
|
|
variant={'outline'}
|
|
colorScheme={'gray'}
|
|
aria-label={'delete'}
|
|
size={'sm'}
|
|
onClick={async () => {
|
|
await delOneKbDataByDataId(item.id);
|
|
refetchData(pageNum);
|
|
}}
|
|
/>
|
|
</Td>
|
|
</Tr>
|
|
))}
|
|
</Tbody>
|
|
</Table>
|
|
</TableContainer>
|
|
<Flex mt={2} justifyContent={'flex-end'}>
|
|
<Pagination />
|
|
</Flex>
|
|
|
|
<Loading loading={isLoading} fixed={false} />
|
|
{editInputData !== undefined && (
|
|
<InputModal
|
|
kbId={kbId}
|
|
defaultValues={editInputData}
|
|
onClose={() => setEditInputData(undefined)}
|
|
onSuccess={refetchData}
|
|
/>
|
|
)}
|
|
{isOpenSelectFileModal && (
|
|
<SelectFileModal kbId={kbId} onClose={onCloseSelectFileModal} onSuccess={refetchData} />
|
|
)}
|
|
{isOpenSelectCsvModal && (
|
|
<SelectCsvModal kbId={kbId} onClose={onCloseSelectCsvModal} onSuccess={refetchData} />
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
export default DataCard;
|