307 lines
8.5 KiB
TypeScript
307 lines
8.5 KiB
TypeScript
import React, { useCallback, useState, useRef, useEffect } 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 { ModelDataItemType } from '@/types/model';
|
||
import { ModelDataStatusMap } from '@/constants/model';
|
||
import { usePagination } from '@/hooks/usePagination';
|
||
import {
|
||
getModelDataList,
|
||
delOneModelData,
|
||
getModelSplitDataListLen,
|
||
getExportDataList
|
||
} from '@/api/model';
|
||
import { DeleteIcon, RepeatIcon, EditIcon } from '@chakra-ui/icons';
|
||
import { useLoading } from '@/hooks/useLoading';
|
||
import { fileDownload } from '@/utils/file';
|
||
import dynamic from 'next/dynamic';
|
||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||
import Papa from 'papaparse';
|
||
import InputModal, { FormData as InputDataType } from './InputDataModal';
|
||
|
||
const SelectFileModal = dynamic(() => import('./SelectFileModal'));
|
||
const SelectCsvModal = dynamic(() => import('./SelectCsvModal'));
|
||
|
||
const ModelDataCard = ({ modelId, isOwner }: { modelId: string; isOwner: boolean }) => {
|
||
const { Loading, setIsLoading } = useLoading();
|
||
const lastSearch = useRef('');
|
||
const [searchText, setSearchText] = useState('');
|
||
const tdStyles = useRef<BoxProps>({
|
||
fontSize: 'xs',
|
||
minW: '150px',
|
||
maxW: '500px',
|
||
maxH: '250px',
|
||
whiteSpace: 'pre-wrap',
|
||
overflowY: 'auto'
|
||
});
|
||
|
||
const {
|
||
data: modelDataList,
|
||
isLoading,
|
||
Pagination,
|
||
total,
|
||
getData,
|
||
pageNum
|
||
} = usePagination<ModelDataItemType>({
|
||
api: getModelDataList,
|
||
pageSize: 10,
|
||
params: {
|
||
modelId,
|
||
searchText
|
||
},
|
||
defaultRequest: false
|
||
});
|
||
|
||
useEffect(() => {
|
||
getData(1);
|
||
}, [modelId, getData]);
|
||
|
||
const [editInputData, setEditInputData] = useState<InputDataType>();
|
||
|
||
const {
|
||
isOpen: isOpenSelectFileModal,
|
||
onOpen: onOpenSelectFileModal,
|
||
onClose: onCloseSelectFileModal
|
||
} = useDisclosure();
|
||
const {
|
||
isOpen: isOpenSelectCsvModal,
|
||
onOpen: onOpenSelectCsvModal,
|
||
onClose: onCloseSelectCsvModal
|
||
} = useDisclosure();
|
||
|
||
const { data: splitDataLen = 0, refetch } = useQuery(
|
||
['getModelSplitDataList'],
|
||
() => getModelSplitDataListLen(modelId),
|
||
{
|
||
onError(err) {
|
||
console.log(err);
|
||
}
|
||
}
|
||
);
|
||
|
||
const refetchData = useCallback(
|
||
(num = 1) => {
|
||
getData(num);
|
||
refetch();
|
||
return null;
|
||
},
|
||
[getData, refetch]
|
||
);
|
||
|
||
useQuery(['refetchData'], () => refetchData(pageNum), {
|
||
refetchInterval: 5000,
|
||
enabled: splitDataLen > 0
|
||
});
|
||
|
||
// 获取所有的数据,并导出 json
|
||
const { mutate: onclickExport, isLoading: isLoadingExport = false } = useMutation({
|
||
mutationFn: () => getExportDataList(modelId),
|
||
onSuccess(res) {
|
||
try {
|
||
setIsLoading(true);
|
||
const text = Papa.unparse({
|
||
fields: ['question', 'answer'],
|
||
data: res
|
||
});
|
||
fileDownload({
|
||
text,
|
||
type: 'text/csv',
|
||
filename: 'data.csv'
|
||
});
|
||
} catch (error) {
|
||
error;
|
||
}
|
||
setIsLoading(false);
|
||
},
|
||
onError(err) {
|
||
console.log(err);
|
||
}
|
||
});
|
||
|
||
return (
|
||
<>
|
||
<Flex>
|
||
<Box fontWeight={'bold'} fontSize={'lg'} flex={1} mr={2}>
|
||
知识库数据: {total}组
|
||
</Box>
|
||
{isOwner && (
|
||
<>
|
||
<IconButton
|
||
icon={<RepeatIcon />}
|
||
aria-label={'refresh'}
|
||
variant={'outline'}
|
||
mr={4}
|
||
size={'sm'}
|
||
onClick={() => refetchData(pageNum)}
|
||
/>
|
||
<Button
|
||
variant={'outline'}
|
||
mr={2}
|
||
size={'sm'}
|
||
isLoading={isLoadingExport}
|
||
title={'换行数据导出时,会进行格式转换'}
|
||
onClick={() => onclickExport()}
|
||
>
|
||
导出
|
||
</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}>
|
||
{isOwner && splitDataLen > 0 && (
|
||
<Box fontSize={'xs'}>{splitDataLen}条数据正在拆分,请耐心等待...</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>
|
||
|
||
<Box mt={4}>
|
||
<TableContainer minH={'500px'}>
|
||
<Table variant={'simple'} w={'100%'}>
|
||
<Thead>
|
||
<Tr>
|
||
<Th>
|
||
匹配的知识点
|
||
<Tooltip
|
||
label={
|
||
'对话时,会将用户的问题和知识库的 "匹配知识点" 进行比较,找到最相似的前 n 条记录,将这些记录的 "匹配知识点"+"补充知识点" 作为 chatgpt 的系统提示词。'
|
||
}
|
||
>
|
||
<QuestionOutlineIcon ml={1} />
|
||
</Tooltip>
|
||
</Th>
|
||
<Th>补充知识</Th>
|
||
<Th>状态</Th>
|
||
{isOwner && <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>{ModelDataStatusMap[item.status]}</Td>
|
||
{isOwner && (
|
||
<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 delOneModelData(item.id);
|
||
refetchData(pageNum);
|
||
}}
|
||
/>
|
||
</Td>
|
||
)}
|
||
</Tr>
|
||
))}
|
||
</Tbody>
|
||
</Table>
|
||
</TableContainer>
|
||
<Box mt={2} textAlign={'end'}>
|
||
<Pagination />
|
||
</Box>
|
||
</Box>
|
||
|
||
<Loading loading={isLoading} fixed={false} />
|
||
{editInputData !== undefined && (
|
||
<InputModal
|
||
modelId={modelId}
|
||
defaultValues={editInputData}
|
||
onClose={() => setEditInputData(undefined)}
|
||
onSuccess={refetchData}
|
||
/>
|
||
)}
|
||
{isOpenSelectFileModal && (
|
||
<SelectFileModal
|
||
modelId={modelId}
|
||
onClose={onCloseSelectFileModal}
|
||
onSuccess={refetchData}
|
||
/>
|
||
)}
|
||
{isOpenSelectCsvModal && (
|
||
<SelectCsvModal modelId={modelId} onClose={onCloseSelectCsvModal} onSuccess={refetchData} />
|
||
)}
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default ModelDataCard;
|