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:
heheer
2025-03-11 19:44:33 +08:00
committed by archer
parent 4655c2754e
commit e061e80235
64 changed files with 2676 additions and 369 deletions

View File

@@ -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;

View File

@@ -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}&currentTab=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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;