chat quote reader (#3912)
* init chat quote full text reader * linked structure * dataset data linked * optimize code * fix ts build * test finish * delete log * fix * fix ts * fix ts * remove nextId * initial scroll * fix * fix
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
import InputDataModal from '@/pageComponents/dataset/detail/InputDataModal';
|
||||
|
||||
const CollectionQuoteItem = ({
|
||||
index,
|
||||
quoteRefs,
|
||||
quoteIndex,
|
||||
setQuoteIndex,
|
||||
refreshList,
|
||||
canEdit,
|
||||
|
||||
updated,
|
||||
isCurrentSelected,
|
||||
q,
|
||||
a,
|
||||
dataId,
|
||||
collectionId
|
||||
}: {
|
||||
index: number;
|
||||
quoteRefs: MutableRefObject<(HTMLDivElement | null)[]>;
|
||||
quoteIndex: number;
|
||||
setQuoteIndex: Dispatch<SetStateAction<number>>;
|
||||
refreshList: () => void;
|
||||
canEdit: boolean;
|
||||
|
||||
updated?: boolean;
|
||||
isCurrentSelected: boolean;
|
||||
q: string;
|
||||
a?: string;
|
||||
dataId: string;
|
||||
collectionId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const hasBeenSearched = quoteIndex !== undefined && quoteIndex > -1;
|
||||
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
ref={(el: HTMLDivElement | null) => {
|
||||
quoteRefs.current[index] = el;
|
||||
}}
|
||||
p={2}
|
||||
py={2}
|
||||
cursor={hasBeenSearched ? 'pointer' : 'default'}
|
||||
bg={isCurrentSelected ? '#FFF9E7' : hasBeenSearched ? '#FFFCF2' : ''}
|
||||
position={'relative'}
|
||||
overflow={'hidden'}
|
||||
border={'1px solid '}
|
||||
borderColor={isCurrentSelected ? 'yellow.200' : 'transparent'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
_hover={
|
||||
hasBeenSearched
|
||||
? {
|
||||
'& .hover-data': { visibility: 'visible' }
|
||||
}
|
||||
: {
|
||||
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
|
||||
borderTopColor: 'myGray.50',
|
||||
'& .hover-data': { visibility: 'visible' }
|
||||
}
|
||||
}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasBeenSearched) {
|
||||
setQuoteIndex(quoteIndex);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{updated && (
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
top={2}
|
||||
right={5}
|
||||
gap={1}
|
||||
bg={'yellow.50'}
|
||||
color={'yellow.500'}
|
||||
px={2}
|
||||
py={1}
|
||||
rounded={'md'}
|
||||
fontSize={'12px'}
|
||||
>
|
||||
<MyIcon name="common/info" w={'14px'} color={'yellow.500'} />
|
||||
{t('common:core.dataset.data.Updated')}
|
||||
</Flex>
|
||||
)}
|
||||
<Markdown source={q} />
|
||||
{!!a && (
|
||||
<Box>
|
||||
<Markdown source={a} />
|
||||
</Box>
|
||||
)}
|
||||
<Flex
|
||||
className="hover-data"
|
||||
position={'absolute'}
|
||||
bottom={2}
|
||||
right={5}
|
||||
gap={1.5}
|
||||
visibility={'hidden'}
|
||||
>
|
||||
<MyTooltip label={t('common:core.dataset.Quote Length')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={2}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
>
|
||||
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{q.length + (a?.length || 0)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
{canEdit && (
|
||||
<MyTooltip label={t('common:core.dataset.data.Edit')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
cursor={'pointer'}
|
||||
onClick={() =>
|
||||
setEditInputData({
|
||||
dataId,
|
||||
collectionId
|
||||
})
|
||||
}
|
||||
>
|
||||
<MyIcon name="common/edit" w={'14px'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={t('common:common.Copy')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
copyData(q + '\n' + a);
|
||||
}}
|
||||
>
|
||||
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Box>
|
||||
{editInputData && (
|
||||
<InputDataModal
|
||||
onClose={() => setEditInputData(undefined)}
|
||||
onSuccess={() => {
|
||||
console.log('onSuccess');
|
||||
refreshList();
|
||||
}}
|
||||
dataId={editInputData.dataId}
|
||||
collectionId={editInputData.collectionId}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionQuoteItem;
|
||||
@@ -0,0 +1,345 @@
|
||||
import { Box, Button, Flex } from '@chakra-ui/react';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import DownloadButton from './DownloadButton';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { downloadFetch } from '@/web/common/system/utils';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { getCollectionSource, getDatasetDataPermission } from '@/web/core/dataset/api';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import ScoreTag from './ScoreTag';
|
||||
import { formatScore } from '@/components/core/dataset/QuoteItem';
|
||||
import NavButton from './NavButton';
|
||||
import { useLinkedScroll } from '@fastgpt/web/hooks/useLinkedScroll';
|
||||
import CollectionQuoteItem from './CollectionQuoteItem';
|
||||
import { DatasetDataListItemType } from '@/global/core/dataset/type';
|
||||
import { metadataType } from '@/web/core/chat/context/chatItemContext';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { getCollectionQuote } from '@/web/core/chat/api';
|
||||
|
||||
const CollectionReader = ({
|
||||
rawSearch,
|
||||
metadata,
|
||||
chatTime,
|
||||
onClose
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
chatTime: Date;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
const { userInfo } = useUserStore();
|
||||
const { collectionId, datasetId, chatItemId, sourceId, sourceName } = metadata;
|
||||
const [quoteIndex, setQuoteIndex] = useState(0);
|
||||
|
||||
const { data: permissionData, loading: isPermissionLoading } = useRequest2(
|
||||
async () => await getDatasetDataPermission(datasetId),
|
||||
{
|
||||
manual: !userInfo && !datasetId,
|
||||
refreshDeps: [datasetId, userInfo]
|
||||
}
|
||||
);
|
||||
|
||||
const filterResults = useMemo(() => {
|
||||
const results = rawSearch.filter(
|
||||
(item) => item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
|
||||
);
|
||||
|
||||
return results.sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
|
||||
}, [metadata, rawSearch]);
|
||||
|
||||
const currentQuoteItem = filterResults[quoteIndex];
|
||||
|
||||
const {
|
||||
dataList: datasetDataList,
|
||||
setDataList: setDatasetDataList,
|
||||
isLoading,
|
||||
loadData,
|
||||
ScrollData,
|
||||
itemRefs,
|
||||
scrollToItem
|
||||
} = useLinkedScroll(getCollectionQuote, {
|
||||
refreshDeps: [collectionId],
|
||||
params: {
|
||||
collectionId,
|
||||
chatTime,
|
||||
chatItemId,
|
||||
chatId,
|
||||
appId,
|
||||
...outLinkAuthData
|
||||
},
|
||||
initialId: currentQuoteItem?.id,
|
||||
initialIndex: currentQuoteItem?.chunkIndex,
|
||||
canLoadData: !!currentQuoteItem?.id && !isPermissionLoading
|
||||
});
|
||||
|
||||
const loading = isLoading || isPermissionLoading;
|
||||
const isDeleted = !datasetDataList.find((item) => item._id === currentQuoteItem?.id);
|
||||
|
||||
const formatedDataList = useMemo(
|
||||
() =>
|
||||
datasetDataList.map((item: DatasetDataListItemType) => {
|
||||
const isCurrentSelected = currentQuoteItem?.id === item._id;
|
||||
const quoteIndex = filterResults.findIndex((res) => res.id === item._id);
|
||||
|
||||
return {
|
||||
...item,
|
||||
isCurrentSelected,
|
||||
quoteIndex
|
||||
};
|
||||
}),
|
||||
[currentQuoteItem?.id, datasetDataList, filterResults]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setQuoteIndex(0);
|
||||
setDatasetDataList([]);
|
||||
}, [collectionId, setDatasetDataList]);
|
||||
|
||||
const { runAsync: handleDownload, loading: downloadLoading } = useRequest2(async () => {
|
||||
await downloadFetch({
|
||||
url: '/api/core/dataset/collection/export',
|
||||
filename: 'parsed_content.md',
|
||||
body: {
|
||||
collectionId: collectionId,
|
||||
chatTime: chatTime,
|
||||
chatItemId: chatItemId
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const { runAsync: handleRead, loading: readLoading } = useRequest2(
|
||||
async () => await getCollectionSource({ ...metadata, appId, chatId }),
|
||||
{
|
||||
onSuccess: (res) => {
|
||||
if (!res.value) {
|
||||
throw new Error('No file found');
|
||||
}
|
||||
if (res.value.startsWith('/')) {
|
||||
window.open(`${location.origin}${res.value}`, '_blank');
|
||||
} else {
|
||||
window.open(res.value, '_blank');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({
|
||||
title: t(getErrText(err, t('common:error.fileNotFound'))),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handleNavigate = useCallback(
|
||||
async (targetIndex: number) => {
|
||||
if (targetIndex < 0 || targetIndex >= filterResults.length) return;
|
||||
const targetItemId = filterResults[targetIndex].id;
|
||||
const targetItemIndex = filterResults[targetIndex].chunkIndex;
|
||||
|
||||
setQuoteIndex(targetIndex);
|
||||
const dataIndex = datasetDataList.findIndex((item) => item._id === targetItemId);
|
||||
|
||||
if (dataIndex !== -1) {
|
||||
setTimeout(() => {
|
||||
scrollToItem(dataIndex);
|
||||
}, 50);
|
||||
} else {
|
||||
try {
|
||||
await loadData({ id: targetItemId, index: targetItemIndex });
|
||||
} catch (error) {
|
||||
console.error('Failed to navigate:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
[filterResults, datasetDataList, scrollToItem, loadData]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'full'}>
|
||||
{/* title */}
|
||||
<Flex
|
||||
w={'full'}
|
||||
alignItems={'center'}
|
||||
px={5}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.150'}
|
||||
>
|
||||
<Box flex={1} py={4}>
|
||||
<Flex mb={1} alignItems={['flex-start', 'center']} flexDirection={['column', 'row']}>
|
||||
<Flex gap={2} mr={2}>
|
||||
<MyIcon
|
||||
name={getSourceNameIcon({ sourceId, sourceName }) as any}
|
||||
w={['1rem', '1.25rem']}
|
||||
color={'primary.600'}
|
||||
/>
|
||||
<Box
|
||||
maxW={['200px', '300px']}
|
||||
className={'textEllipsis'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{sourceName || t('common:common.UnKnow Source')}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex gap={3} mt={[2, 0]} alignItems={'center'}>
|
||||
{!!userInfo && permissionData?.permission?.hasReadPer && (
|
||||
<Button
|
||||
variant={'primaryGhost'}
|
||||
size={'xs'}
|
||||
fontSize={'mini'}
|
||||
border={'none'}
|
||||
_hover={{
|
||||
bg: 'primary.100'
|
||||
}}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
`/dataset/detail?datasetId=${datasetId}¤tTab=dataCard&collectionId=${collectionId}`
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t('common:core.dataset.Go Dataset')}
|
||||
<MyIcon name="common/upperRight" w={4} ml={1} />
|
||||
</Button>
|
||||
)}
|
||||
<DownloadButton
|
||||
canAccessRawData={true}
|
||||
onDownload={handleDownload}
|
||||
onRead={handleRead}
|
||||
isLoading={downloadLoading || readLoading}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Box fontSize={'mini'} color={'myGray.500'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
borderRadius={'sm'}
|
||||
p={1}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* header control */}
|
||||
{datasetDataList.length > 0 && (
|
||||
<Flex
|
||||
w={'full'}
|
||||
px={4}
|
||||
py={2}
|
||||
alignItems={'center'}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.150'}
|
||||
>
|
||||
{/* 引用序号 */}
|
||||
<Flex fontSize={'mini'} mr={3} alignItems={'center'} gap={1}>
|
||||
<Box as={'span'} color={'myGray.900'}>
|
||||
{t('common:core.chat.Quote')} {quoteIndex + 1}
|
||||
</Box>
|
||||
<Box as={'span'} color={'myGray.500'}>
|
||||
/
|
||||
</Box>
|
||||
<Box as={'span'} color={'myGray.500'}>
|
||||
{filterResults.length}
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* 检索分数 */}
|
||||
{!loading &&
|
||||
(!isDeleted ? (
|
||||
<ScoreTag {...formatScore(currentQuoteItem?.score)} />
|
||||
) : (
|
||||
<Flex
|
||||
borderRadius={'sm'}
|
||||
py={1}
|
||||
px={2}
|
||||
color={'red.600'}
|
||||
bg={'red.50'}
|
||||
alignItems={'center'}
|
||||
fontSize={'11px'}
|
||||
>
|
||||
<MyIcon name="common/info" w={'14px'} mr={1} color={'red.600'} />
|
||||
{t('chat:chat.quote.deleted')}
|
||||
</Flex>
|
||||
))}
|
||||
|
||||
<Box flex={1} />
|
||||
|
||||
{/* 检索按钮 */}
|
||||
<Flex gap={1}>
|
||||
<NavButton
|
||||
direction="up"
|
||||
isDisabled={quoteIndex === 0}
|
||||
onClick={() => handleNavigate(quoteIndex - 1)}
|
||||
/>
|
||||
<NavButton
|
||||
direction="down"
|
||||
isDisabled={quoteIndex === filterResults.length - 1}
|
||||
onClick={() => handleNavigate(quoteIndex + 1)}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{/* quote list */}
|
||||
{loading || datasetDataList.length > 0 ? (
|
||||
<ScrollData flex={'1 0 0'} mt={2} px={5} py={1} isLoading={loading}>
|
||||
<Flex flexDir={'column'} gap={3}>
|
||||
{formatedDataList.map((item, index) => (
|
||||
<CollectionQuoteItem
|
||||
key={item._id}
|
||||
index={index}
|
||||
quoteRefs={itemRefs as React.MutableRefObject<(HTMLDivElement | null)[]>}
|
||||
quoteIndex={item.quoteIndex}
|
||||
setQuoteIndex={setQuoteIndex}
|
||||
refreshList={() =>
|
||||
currentQuoteItem?.id &&
|
||||
loadData({ id: currentQuoteItem.id, index: currentQuoteItem.chunkIndex })
|
||||
}
|
||||
updated={item.updated}
|
||||
isCurrentSelected={item.isCurrentSelected}
|
||||
q={item.q}
|
||||
a={item.a}
|
||||
dataId={item._id}
|
||||
collectionId={collectionId}
|
||||
canEdit={!!userInfo && !!permissionData?.permission?.hasWritePer}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</ScrollData>
|
||||
) : (
|
||||
<Flex
|
||||
flex={'1 0 0'}
|
||||
flexDirection={'column'}
|
||||
gap={1}
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
<Box border={'1px dashed'} borderColor={'myGray.400'} p={2} borderRadius={'full'}>
|
||||
<MyIcon name="common/fileNotFound" />
|
||||
</Box>
|
||||
<Box fontSize={'sm'} color={'myGray.500'}>
|
||||
{t('chat:chat.quote.No Data')}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionReader;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Button } from '@chakra-ui/react';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const DownloadButton = ({
|
||||
canAccessRawData,
|
||||
onDownload,
|
||||
onRead,
|
||||
isLoading
|
||||
}: {
|
||||
canAccessRawData: boolean;
|
||||
onDownload: () => void;
|
||||
onRead: () => void;
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (canAccessRawData) {
|
||||
return (
|
||||
<MyMenu
|
||||
size={'xs'}
|
||||
Button={
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={'xs'}
|
||||
fontSize={'mini'}
|
||||
leftIcon={<MyIcon name={'common/download'} w={'4'} />}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('common:Download')}
|
||||
</Button>
|
||||
}
|
||||
menuList={[
|
||||
{
|
||||
children: [
|
||||
{
|
||||
label: t('common:core.dataset.Download the parsed content'),
|
||||
type: 'grayBg',
|
||||
onClick: onDownload
|
||||
},
|
||||
{
|
||||
label: t('common:core.dataset.Get the raw data'),
|
||||
type: 'grayBg',
|
||||
onClick: onRead
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={'whitePrimary'}
|
||||
size={'xs'}
|
||||
fontSize={'mini'}
|
||||
leftIcon={<MyIcon name={'common/download'} w={'4'} />}
|
||||
onClick={onDownload}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{t('common:Download')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default DownloadButton;
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const NavButton = ({
|
||||
direction,
|
||||
isDisabled,
|
||||
onClick
|
||||
}: {
|
||||
direction: 'up' | 'down';
|
||||
isDisabled: boolean;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const isUp = direction === 'up';
|
||||
|
||||
const baseStyles = {
|
||||
color: 'myGray.500',
|
||||
border: '1px solid',
|
||||
borderColor: 'myGray.150',
|
||||
borderRadius: 'sm',
|
||||
w: 6,
|
||||
h: 6,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'all 0.2s'
|
||||
};
|
||||
|
||||
const stateStyles = isDisabled
|
||||
? {
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.5,
|
||||
_hover: {}
|
||||
}
|
||||
: {
|
||||
cursor: 'pointer',
|
||||
opacity: 1,
|
||||
_hover: { bg: 'myGray.100' },
|
||||
onClick
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex {...baseStyles} {...stateStyles}>
|
||||
<MyIcon name={isUp ? `common/solidChevronUp` : `common/solidChevronDown`} w={'18px'} />
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavButton;
|
||||
163
projects/app/src/pageComponents/chat/ChatQuoteList/QuoteItem.tsx
Normal file
163
projects/app/src/pageComponents/chat/ChatQuoteList/QuoteItem.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { ScoreItemType } from '@/components/core/dataset/QuoteItem';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import ScoreTag from './ScoreTag';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
|
||||
|
||||
const QuoteItem = ({
|
||||
index,
|
||||
icon,
|
||||
sourceName,
|
||||
score,
|
||||
q,
|
||||
a
|
||||
}: {
|
||||
index: number;
|
||||
icon: string;
|
||||
sourceName: string;
|
||||
score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] };
|
||||
q: string;
|
||||
a?: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const isDeleted = !q;
|
||||
|
||||
return (
|
||||
<Box
|
||||
p={2}
|
||||
position={'relative'}
|
||||
overflow={'hidden'}
|
||||
border={'1px solid transparent'}
|
||||
borderBottomColor={'myGray.150'}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
_hover={{
|
||||
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
|
||||
borderTopColor: 'myGray.50',
|
||||
'& .hover-data': { visibility: 'visible' }
|
||||
}}
|
||||
>
|
||||
<Flex gap={2} alignItems={'center'} mb={2}>
|
||||
<Box
|
||||
alignItems={'center'}
|
||||
fontSize={'xs'}
|
||||
border={'sm'}
|
||||
borderRadius={'sm'}
|
||||
_hover={{
|
||||
'.controller': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
overflow={'hidden'}
|
||||
display={'inline-flex'}
|
||||
height={6}
|
||||
>
|
||||
<Flex
|
||||
color={'myGray.500'}
|
||||
bg={'myGray.150'}
|
||||
w={4}
|
||||
justifyContent={'center'}
|
||||
fontSize={'10px'}
|
||||
h={'full'}
|
||||
alignItems={'center'}
|
||||
>
|
||||
{index + 1}
|
||||
</Flex>
|
||||
<Flex px={1.5}>
|
||||
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
|
||||
<Box
|
||||
className="textEllipsis3"
|
||||
wordBreak={'break-all'}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'mini'}
|
||||
color={'myGray.900'}
|
||||
>
|
||||
{sourceName}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
{score && !isDeleted && (
|
||||
<Box className="hover-data" visibility={'hidden'}>
|
||||
<ScoreTag {...score} />
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
{!isDeleted ? (
|
||||
<>
|
||||
<Markdown source={q} />
|
||||
{!!a && (
|
||||
<Box>
|
||||
<Markdown source={a} />
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Flex
|
||||
justifyContent={'center'}
|
||||
alignItems={'center'}
|
||||
h={'full'}
|
||||
py={2}
|
||||
bg={'#FAFAFA'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
<MyIcon name="common/info" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{t('chat:chat.quote.deleted')}
|
||||
</Flex>
|
||||
)}
|
||||
<Flex
|
||||
className="hover-data"
|
||||
position={'absolute'}
|
||||
bottom={2}
|
||||
right={5}
|
||||
gap={1.5}
|
||||
visibility={'hidden'}
|
||||
>
|
||||
<MyTooltip label={t('common:core.dataset.Quote Length')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={2}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
>
|
||||
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
|
||||
{q.length + (a?.length || 0)}
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
<MyTooltip label={t('common:common.Copy')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'10px'}
|
||||
border={'1px solid'}
|
||||
borderColor={'myGray.200'}
|
||||
bg={'white'}
|
||||
rounded={'sm'}
|
||||
px={1}
|
||||
py={1}
|
||||
boxShadow={
|
||||
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
copyData(q + '\n' + a);
|
||||
}}
|
||||
>
|
||||
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteItem;
|
||||
@@ -0,0 +1,152 @@
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import QuoteItem from './QuoteItem';
|
||||
import { useMemo } from 'react';
|
||||
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
|
||||
import { formatScore } from '@/components/core/dataset/QuoteItem';
|
||||
import { metadataType } from '@/web/core/chat/context/chatItemContext';
|
||||
import { getQuoteDataList } from '@/web/core/chat/api';
|
||||
|
||||
const QuoteReader = ({
|
||||
rawSearch,
|
||||
metadata,
|
||||
chatTime,
|
||||
onClose
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
chatTime: Date;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
|
||||
const { data, loading } = useRequest2(
|
||||
async () =>
|
||||
await getQuoteDataList({
|
||||
datasetDataIdList: rawSearch.map((item) => item.id),
|
||||
chatTime,
|
||||
collectionIdList: metadata.collectionIdList,
|
||||
chatItemId: metadata.chatItemId,
|
||||
appId,
|
||||
chatId,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const filterResults = useMemo(() => {
|
||||
if (!metadata.collectionId) {
|
||||
return rawSearch;
|
||||
}
|
||||
|
||||
return rawSearch.filter(
|
||||
(item) => item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
|
||||
);
|
||||
}, [metadata, rawSearch]);
|
||||
|
||||
const formatedDataList = useMemo(() => {
|
||||
return filterResults
|
||||
.map((item) => {
|
||||
const currentFilterItem = data?.quoteList.find((res) => res._id === item.id);
|
||||
|
||||
return {
|
||||
...item,
|
||||
q: currentFilterItem?.q || '',
|
||||
a: currentFilterItem?.a || '',
|
||||
score: formatScore(item.score),
|
||||
icon: getSourceNameIcon({
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName
|
||||
})
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return (b.score.primaryScore?.value || 0) - (a.score.primaryScore?.value || 0);
|
||||
});
|
||||
}, [data?.quoteList, filterResults]);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'full'}>
|
||||
{/* title */}
|
||||
<Flex
|
||||
w={'full'}
|
||||
alignItems={'center'}
|
||||
px={5}
|
||||
borderBottom={'1px solid'}
|
||||
borderColor={'myGray.150'}
|
||||
>
|
||||
<Box flex={1} py={4}>
|
||||
<Flex gap={2} mr={2} mb={1}>
|
||||
<MyIcon
|
||||
name={
|
||||
metadata.sourceId && metadata.sourceName
|
||||
? (getSourceNameIcon({
|
||||
sourceId: metadata.sourceId,
|
||||
sourceName: metadata.sourceName
|
||||
}) as any)
|
||||
: 'core/chat/quoteFill'
|
||||
}
|
||||
w={['1rem', '1.25rem']}
|
||||
color={'primary.600'}
|
||||
/>
|
||||
<Box
|
||||
maxW={['200px', '300px']}
|
||||
className={'textEllipsis'}
|
||||
wordBreak={'break-all'}
|
||||
color={'myGray.900'}
|
||||
fontWeight={'medium'}
|
||||
>
|
||||
{metadata.sourceName
|
||||
? metadata.sourceName
|
||||
: t('common:core.chat.Quote Amount', { amount: rawSearch.length })}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box fontSize={'mini'} color={'myGray.500'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
cursor={'pointer'}
|
||||
borderRadius={'sm'}
|
||||
p={1}
|
||||
_hover={{
|
||||
bg: 'myGray.100'
|
||||
}}
|
||||
onClick={onClose}
|
||||
>
|
||||
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
{/* quote list */}
|
||||
<MyBox flex={'1 0 0'} mt={2} px={5} py={1} overflow={'auto'} isLoading={loading}>
|
||||
{!loading && (
|
||||
<Flex flexDir={'column'} gap={3}>
|
||||
{formatedDataList?.map((item, index) => (
|
||||
<QuoteItem
|
||||
key={item.id}
|
||||
index={index}
|
||||
icon={item.icon}
|
||||
sourceName={item.sourceName}
|
||||
score={item.score}
|
||||
q={item.q}
|
||||
a={item.a}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</MyBox>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteReader;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ScoreItemType, scoreTheme } from '@/components/core/dataset/QuoteItem';
|
||||
import { Box, Flex, Progress } from '@chakra-ui/react';
|
||||
import { SearchScoreTypeMap } from '@fastgpt/global/core/dataset/constants';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const ScoreTag = (score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Flex alignItems={'center'} flexWrap={'wrap'} gap={3}>
|
||||
{score?.primaryScore && (
|
||||
<MyTooltip
|
||||
label={
|
||||
score.secondaryScore.length ? (
|
||||
<Box>
|
||||
{score.secondaryScore.map((item, i) => (
|
||||
<Box fontSize={'xs'} key={i}>
|
||||
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
|
||||
<Box
|
||||
px={'5px'}
|
||||
borderWidth={'1px'}
|
||||
borderRadius={'sm'}
|
||||
mr={'2px'}
|
||||
{...(scoreTheme[i] && scoreTheme[i])}
|
||||
>
|
||||
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
|
||||
</Box>
|
||||
<Box transform={'scale(0.9)'}>
|
||||
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Box h={'4px'}>
|
||||
{SearchScoreTypeMap[item.type]?.showScore && (
|
||||
<Progress
|
||||
value={item.value * 100}
|
||||
h={'4px'}
|
||||
w={'100%'}
|
||||
size="sm"
|
||||
borderRadius={'20px'}
|
||||
{...(scoreTheme[i] && {
|
||||
colorScheme: scoreTheme[i].colorScheme
|
||||
})}
|
||||
bg="#E8EBF0"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
) : (
|
||||
t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)
|
||||
)
|
||||
}
|
||||
>
|
||||
<Flex
|
||||
borderRadius={'sm'}
|
||||
py={1}
|
||||
px={2}
|
||||
color={'green.600'}
|
||||
bg={'green.50'}
|
||||
alignItems={'center'}
|
||||
fontSize={'11px'}
|
||||
>
|
||||
<Box>
|
||||
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
|
||||
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
|
||||
? ` ${score.primaryScore.value.toFixed(4)}`
|
||||
: `: ${score.primaryScore.index + 1}`}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScoreTag;
|
||||
42
projects/app/src/pageComponents/chat/ChatQuoteList/index.tsx
Normal file
42
projects/app/src/pageComponents/chat/ChatQuoteList/index.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatItemContext, metadataType } from '@/web/core/chat/context/chatItemContext';
|
||||
import CollectionQuoteReader from './CollectionQuoteReader';
|
||||
import QuoteReader from './QuoteReader';
|
||||
|
||||
const ChatQuoteList = ({
|
||||
chatTime,
|
||||
rawSearch = [],
|
||||
metadata,
|
||||
onClose
|
||||
}: {
|
||||
chatTime: Date;
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const isShowReadRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
|
||||
return (
|
||||
<>
|
||||
{metadata.collectionId && isShowReadRawSource ? (
|
||||
<CollectionQuoteReader
|
||||
rawSearch={rawSearch}
|
||||
metadata={metadata}
|
||||
chatTime={chatTime}
|
||||
onClose={onClose}
|
||||
/>
|
||||
) : (
|
||||
<QuoteReader
|
||||
rawSearch={rawSearch}
|
||||
metadata={metadata}
|
||||
chatTime={chatTime}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatQuoteList;
|
||||
Reference in New Issue
Block a user