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:
@@ -1,22 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import type { BoxProps } from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
interface Props extends BoxProps {}
|
||||
interface Props extends BoxProps {
|
||||
externalTrigger?: Boolean;
|
||||
}
|
||||
|
||||
const SideBar = (e?: Props) => {
|
||||
const {
|
||||
w = ['100%', '0 0 250px', '0 0 270px', '0 0 290px', '0 0 310px'],
|
||||
children,
|
||||
externalTrigger,
|
||||
...props
|
||||
} = e || {};
|
||||
|
||||
const [foldSideBar, setFoldSideBar] = useState(false);
|
||||
const [isFolded, setIsFolded] = useState(false);
|
||||
|
||||
const prevExternalTriggerRef = useRef<Boolean | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (externalTrigger && !prevExternalTriggerRef.current && !isFolded) {
|
||||
setIsFolded(true);
|
||||
}
|
||||
|
||||
prevExternalTriggerRef.current = externalTrigger;
|
||||
}, [externalTrigger, isFolded]);
|
||||
|
||||
const handleToggle = () => {
|
||||
const newFolded = !isFolded;
|
||||
setIsFolded(newFolded);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
position={'relative'}
|
||||
flex={foldSideBar ? '0 0 0' : w}
|
||||
flex={isFolded ? '0 0 0' : w}
|
||||
w={['100%', 0]}
|
||||
h={'100%'}
|
||||
zIndex={1}
|
||||
@@ -40,7 +59,7 @@ const SideBar = (e?: Props) => {
|
||||
bg={'rgba(0,0,0,0.5)'}
|
||||
cursor={'pointer'}
|
||||
transition={'0.2s'}
|
||||
{...(foldSideBar
|
||||
{...(isFolded
|
||||
? {
|
||||
opacity: 0.6
|
||||
}
|
||||
@@ -48,16 +67,16 @@ const SideBar = (e?: Props) => {
|
||||
visibility: 'hidden',
|
||||
opacity: 0
|
||||
})}
|
||||
onClick={() => setFoldSideBar(!foldSideBar)}
|
||||
onClick={handleToggle}
|
||||
>
|
||||
<MyIcon
|
||||
name={'common/backLight'}
|
||||
transform={foldSideBar ? 'rotate(180deg)' : ''}
|
||||
transform={isFolded ? 'rotate(180deg)' : ''}
|
||||
w={'14px'}
|
||||
color={'white'}
|
||||
/>
|
||||
</Flex>
|
||||
<Box position={'relative'} h={'100%'} overflow={foldSideBar ? 'hidden' : 'visible'}>
|
||||
<Box position={'relative'} h={'100%'} overflow={isFolded ? 'hidden' : 'visible'}>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Box, useTheme } from '@chakra-ui/react';
|
||||
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import QuoteItem, { formatScore } from '@/components/core/dataset/QuoteItem';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { getQuoteDataList } from '@/web/core/chat/api';
|
||||
|
||||
const QuoteList = React.memo(function QuoteList({
|
||||
chatItemId,
|
||||
rawSearch = [],
|
||||
chatTime
|
||||
}: {
|
||||
chatItemId?: string;
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
chatTime: Date;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const { chatId, appId, outLinkAuthData } = useChatStore();
|
||||
|
||||
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
|
||||
chatItemId,
|
||||
appId: v.appId,
|
||||
chatId: v.chatId,
|
||||
...(v.outLinkAuthData || {})
|
||||
}));
|
||||
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
const showRouteToDatasetDetail = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.showRouteToDatasetDetail
|
||||
);
|
||||
|
||||
const { data } = useRequest2(
|
||||
async () =>
|
||||
await getQuoteDataList({
|
||||
datasetDataIdList: rawSearch.map((item) => item.id),
|
||||
chatTime,
|
||||
collectionIdList: [...new Set(rawSearch.map((item) => item.collectionId))],
|
||||
chatItemId: chatItemId || '',
|
||||
appId,
|
||||
chatId,
|
||||
...outLinkAuthData
|
||||
}),
|
||||
{
|
||||
manual: false
|
||||
}
|
||||
);
|
||||
|
||||
const formatedDataList = useMemo(() => {
|
||||
return rawSearch
|
||||
.map((item) => {
|
||||
const currentFilterItem = data?.quoteList.find((res) => res._id === item.id);
|
||||
|
||||
return {
|
||||
...item,
|
||||
q: currentFilterItem?.q || '',
|
||||
a: currentFilterItem?.a || ''
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aScore = formatScore(a.score);
|
||||
const bScore = formatScore(b.score);
|
||||
return (bScore.primaryScore?.value || 0) - (aScore.primaryScore?.value || 0);
|
||||
});
|
||||
}, [data?.quoteList, rawSearch]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{formatedDataList.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
flex={'1 0 0'}
|
||||
p={2}
|
||||
borderRadius={'sm'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
_hover={{ '& .hover-data': { display: 'flex' } }}
|
||||
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
|
||||
>
|
||||
<QuoteItem
|
||||
quoteItem={item}
|
||||
canViewSource={showRawSource}
|
||||
canEditDataset={showRouteToDatasetDetail}
|
||||
{...RawSourceBoxProps}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default QuoteList;
|
||||
@@ -1,129 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
|
||||
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
import QuoteItem from '@/components/core/dataset/QuoteItem';
|
||||
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
|
||||
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
|
||||
const QuoteModal = ({
|
||||
rawSearch = [],
|
||||
onClose,
|
||||
chatItemId,
|
||||
metadata
|
||||
}: {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
onClose: () => void;
|
||||
chatItemId: string;
|
||||
metadata?: {
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
sourceName: string;
|
||||
};
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const filterResults = useMemo(
|
||||
() =>
|
||||
metadata
|
||||
? rawSearch.filter(
|
||||
(item) =>
|
||||
item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
|
||||
)
|
||||
: rawSearch,
|
||||
[metadata, rawSearch]
|
||||
);
|
||||
|
||||
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
|
||||
appId: v.appId,
|
||||
chatId: v.chatId,
|
||||
chatItemId,
|
||||
...(v.outLinkAuthData || {})
|
||||
}));
|
||||
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
const showRouteToDatasetDetail = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.showRouteToDatasetDetail
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
h={['90vh', '80vh']}
|
||||
isCentered
|
||||
minW={['90vw', '600px']}
|
||||
iconSrc={!!metadata ? undefined : getWebReqUrl('/imgs/modal/quote.svg')}
|
||||
title={
|
||||
<Box>
|
||||
{metadata ? (
|
||||
<RawSourceBox {...metadata} {...RawSourceBoxProps} canView={showRawSource} />
|
||||
) : (
|
||||
<>{t('common:core.chat.Quote Amount', { amount: rawSearch.length })}</>
|
||||
)}
|
||||
<Box fontSize={'xs'} color={'myGray.500'} fontWeight={'normal'}>
|
||||
{t('common:core.chat.quote.Quote Tip')}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ModalBody>
|
||||
<QuoteList rawSearch={filterResults} chatItemId={chatItemId} />
|
||||
</ModalBody>
|
||||
</MyModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default QuoteModal;
|
||||
|
||||
export const QuoteList = React.memo(function QuoteList({
|
||||
chatItemId,
|
||||
rawSearch = []
|
||||
}: {
|
||||
chatItemId?: string;
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
|
||||
chatItemId,
|
||||
appId: v.appId,
|
||||
chatId: v.chatId,
|
||||
...(v.outLinkAuthData || {})
|
||||
}));
|
||||
const showRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
|
||||
const showRouteToDatasetDetail = useContextSelector(
|
||||
ChatItemContext,
|
||||
(v) => v.showRouteToDatasetDetail
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rawSearch.map((item, i) => (
|
||||
<Box
|
||||
key={i}
|
||||
flex={'1 0 0'}
|
||||
p={2}
|
||||
borderRadius={'sm'}
|
||||
border={theme.borders.base}
|
||||
_notLast={{ mb: 2 }}
|
||||
_hover={{ '& .hover-data': { display: 'flex' } }}
|
||||
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
|
||||
>
|
||||
<QuoteItem
|
||||
quoteItem={item}
|
||||
canViewSource={showRawSource}
|
||||
canEditDataset={showRouteToDatasetDetail}
|
||||
{...RawSourceBoxProps}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -14,8 +14,8 @@ import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
|
||||
import { useSize } from 'ahooks';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../Provider';
|
||||
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'));
|
||||
const ContextModal = dynamic(() => import('./ContextModal'));
|
||||
const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
|
||||
|
||||
@@ -30,6 +30,7 @@ const ResponseTags = ({
|
||||
const { t } = useTranslation();
|
||||
const quoteListRef = React.useRef<HTMLDivElement>(null);
|
||||
const dataId = historyItem.dataId;
|
||||
const chatTime = historyItem.time || new Date();
|
||||
|
||||
const {
|
||||
totalQuoteList: quoteList = [],
|
||||
@@ -38,17 +39,12 @@ const ResponseTags = ({
|
||||
historyPreviewLength = 0
|
||||
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
|
||||
|
||||
const [quoteModalData, setQuoteModalData] = useState<{
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata?: {
|
||||
collectionId: string;
|
||||
sourceId?: string;
|
||||
sourceName: string;
|
||||
};
|
||||
}>();
|
||||
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
|
||||
|
||||
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
|
||||
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const notSharePage = useMemo(() => chatType !== 'share', [chatType]);
|
||||
|
||||
const {
|
||||
@@ -81,7 +77,8 @@ const ResponseTags = ({
|
||||
sourceName: item.sourceName,
|
||||
sourceId: item.sourceId,
|
||||
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
|
||||
collectionId: item.collectionId
|
||||
collectionId: item.collectionId,
|
||||
datasetId: item.datasetId
|
||||
}));
|
||||
}, [quoteList]);
|
||||
|
||||
@@ -99,7 +96,11 @@ const ResponseTags = ({
|
||||
<>
|
||||
<Flex justifyContent={'space-between'} alignItems={'center'}>
|
||||
<Box width={'100%'}>
|
||||
<ChatBoxDivider icon="core/chat/quoteFill" text={t('common:core.chat.Quote')} />
|
||||
<ChatBoxDivider
|
||||
icon="core/chat/quoteFill"
|
||||
text={t('common:core.chat.Quote')}
|
||||
iconColor="#E82F72"
|
||||
/>
|
||||
</Box>
|
||||
{quoteFolded && quoteIsOverflow && (
|
||||
<MyIcon
|
||||
@@ -135,15 +136,13 @@ const ResponseTags = ({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{sourceList.map((item) => {
|
||||
{sourceList.map((item, index) => {
|
||||
return (
|
||||
<MyTooltip key={item.collectionId} label={t('common:core.chat.quote.Read Quote')}>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
fontSize={'xs'}
|
||||
border={'sm'}
|
||||
py={1.5}
|
||||
px={2}
|
||||
borderRadius={'sm'}
|
||||
_hover={{
|
||||
'.controller': {
|
||||
@@ -155,20 +154,46 @@ const ResponseTags = ({
|
||||
cursor={'pointer'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setQuoteModalData({
|
||||
|
||||
setQuoteData({
|
||||
chatTime,
|
||||
rawSearch: quoteList,
|
||||
metadata: {
|
||||
collectionId: item.collectionId,
|
||||
sourceId: item.sourceId,
|
||||
sourceName: item.sourceName
|
||||
collectionIdList: [
|
||||
...new Set(quoteList.map((item) => item.collectionId))
|
||||
],
|
||||
sourceId: item.sourceId || '',
|
||||
sourceName: item.sourceName,
|
||||
datasetId: item.datasetId,
|
||||
chatItemId: historyItem.dataId
|
||||
}
|
||||
});
|
||||
}}
|
||||
height={6}
|
||||
>
|
||||
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
|
||||
<Box className="textEllipsis3" wordBreak={'break-all'} flex={'1 0 0'}>
|
||||
{item.sourceName}
|
||||
</Box>
|
||||
<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={item.icon as any} mr={1} flexShrink={0} w={'12px'} />
|
||||
<Box
|
||||
className="textEllipsis3"
|
||||
wordBreak={'break-all'}
|
||||
flex={'1 0 0'}
|
||||
fontSize={'mini'}
|
||||
>
|
||||
{item.sourceName}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</MyTooltip>
|
||||
);
|
||||
@@ -196,7 +221,22 @@ const ResponseTags = ({
|
||||
colorSchema="blue"
|
||||
type="borderSolid"
|
||||
cursor={'pointer'}
|
||||
onClick={() => setQuoteModalData({ rawSearch: quoteList })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
setQuoteData({
|
||||
chatTime,
|
||||
rawSearch: quoteList,
|
||||
metadata: {
|
||||
collectionId: '',
|
||||
collectionIdList: [...new Set(quoteList.map((item) => item.collectionId))],
|
||||
chatItemId: historyItem.dataId,
|
||||
sourceId: '',
|
||||
sourceName: '',
|
||||
datasetId: ''
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('chat:citations', { num: quoteList.length })}
|
||||
</MyTag>
|
||||
@@ -246,15 +286,10 @@ const ResponseTags = ({
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
{!!quoteModalData && (
|
||||
<QuoteModal
|
||||
{...quoteModalData}
|
||||
chatItemId={historyItem.dataId}
|
||||
onClose={() => setQuoteModalData(undefined)}
|
||||
/>
|
||||
)}
|
||||
{isOpenContextModal && <ContextModal dataId={dataId} onClose={onCloseContextModal} />}
|
||||
{isOpenWholeModal && <WholeResponseModal dataId={dataId} onClose={onCloseWholeModal} />}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal dataId={dataId} chatTime={chatTime} onClose={onCloseWholeModal} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,12 +12,18 @@ const RenderResponseDetail = () => {
|
||||
const isChatting = useContextSelector(PluginRunContext, (v) => v.isChatting);
|
||||
|
||||
const responseData = chatRecords?.[1]?.responseData || [];
|
||||
const chatTime = new Date();
|
||||
|
||||
return isChatting ? (
|
||||
<>{t('chat:in_progress')}</>
|
||||
) : (
|
||||
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
|
||||
<ResponseBox useMobile={true} response={responseData} dataId={chatRecords?.[1]?.dataId} />
|
||||
<ResponseBox
|
||||
useMobile={true}
|
||||
response={responseData}
|
||||
dataId={chatRecords?.[1]?.dataId}
|
||||
chatTime={chatTime}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,11 +3,19 @@ import React from 'react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import type { IconNameType } from '@fastgpt/web/components/common/Icon/type.d';
|
||||
|
||||
const ChatBoxDivider = ({ icon, text }: { icon: IconNameType; text: string }) => {
|
||||
const ChatBoxDivider = ({
|
||||
icon,
|
||||
text,
|
||||
iconColor
|
||||
}: {
|
||||
icon: IconNameType;
|
||||
text: string;
|
||||
iconColor?: string;
|
||||
}) => {
|
||||
return (
|
||||
<Box>
|
||||
<Flex alignItems={'center'} py={2} gap={2}>
|
||||
<MyIcon name={icon} w={'14px'} color={'myGray.900'} />
|
||||
<MyIcon name={icon} w={'14px'} color={iconColor || 'myGray.900'} />
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{text}
|
||||
</Box>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTranslation } from 'next-i18next';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal';
|
||||
import QuoteList from '../ChatContainer/ChatBox/components/QuoteList';
|
||||
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
|
||||
import { formatNumber } from '@fastgpt/global/common/math/tools';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
@@ -32,11 +32,13 @@ type sideTabItemType = {
|
||||
export const WholeResponseContent = ({
|
||||
activeModule,
|
||||
hideTabs,
|
||||
dataId
|
||||
dataId,
|
||||
chatTime
|
||||
}: {
|
||||
activeModule: ChatHistoryItemResType;
|
||||
hideTabs?: boolean;
|
||||
dataId?: string;
|
||||
chatTime?: Date;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -263,7 +265,13 @@ export const WholeResponseContent = ({
|
||||
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
|
||||
<Row
|
||||
label={t('common:core.chat.response.module quoteList')}
|
||||
rawDom={<QuoteList chatItemId={dataId} rawSearch={activeModule.quoteList} />}
|
||||
rawDom={
|
||||
<QuoteList
|
||||
chatItemId={dataId}
|
||||
chatTime={chatTime || new Date()}
|
||||
rawSearch={activeModule.quoteList}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
@@ -562,11 +570,13 @@ const SideTabItem = ({
|
||||
export const ResponseBox = React.memo(function ResponseBox({
|
||||
response,
|
||||
dataId,
|
||||
chatTime,
|
||||
hideTabs = false,
|
||||
useMobile = false
|
||||
}: {
|
||||
response: ChatHistoryItemResType[];
|
||||
dataId?: string;
|
||||
chatTime: Date;
|
||||
hideTabs?: boolean;
|
||||
useMobile?: boolean;
|
||||
}) {
|
||||
@@ -689,7 +699,12 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={'5 0 0'} w={0} height={'100%'}>
|
||||
<WholeResponseContent dataId={dataId} activeModule={activeModule} hideTabs={hideTabs} />
|
||||
<WholeResponseContent
|
||||
dataId={dataId}
|
||||
activeModule={activeModule}
|
||||
hideTabs={hideTabs}
|
||||
chatTime={chatTime}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
) : (
|
||||
@@ -753,6 +768,7 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
dataId={dataId}
|
||||
activeModule={activeModule}
|
||||
hideTabs={hideTabs}
|
||||
chatTime={chatTime}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
@@ -763,7 +779,15 @@ export const ResponseBox = React.memo(function ResponseBox({
|
||||
);
|
||||
});
|
||||
|
||||
const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId: string }) => {
|
||||
const WholeResponseModal = ({
|
||||
onClose,
|
||||
dataId,
|
||||
chatTime
|
||||
}: {
|
||||
onClose: () => void;
|
||||
dataId: string;
|
||||
chatTime: Date;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v);
|
||||
@@ -792,7 +816,7 @@ const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId:
|
||||
}
|
||||
>
|
||||
{!!response?.length ? (
|
||||
<ResponseBox response={response} dataId={dataId} />
|
||||
<ResponseBox response={response} dataId={dataId} chatTime={chatTime} />
|
||||
) : (
|
||||
<EmptyTip text={t('chat:no_workflow_response')} />
|
||||
)}
|
||||
|
||||
@@ -14,8 +14,8 @@ import Markdown from '@/components/Markdown';
|
||||
|
||||
const InputDataModal = dynamic(() => import('@/pageComponents/dataset/detail/InputDataModal'));
|
||||
|
||||
type ScoreItemType = SearchDataResponseItemType['score'][0];
|
||||
const scoreTheme: Record<
|
||||
export type ScoreItemType = SearchDataResponseItemType['score'][0];
|
||||
export const scoreTheme: Record<
|
||||
string,
|
||||
{
|
||||
color: string;
|
||||
@@ -44,6 +44,47 @@ const scoreTheme: Record<
|
||||
}
|
||||
};
|
||||
|
||||
export const formatScore = (score: ScoreItemType[]) => {
|
||||
if (!Array.isArray(score)) {
|
||||
return {
|
||||
primaryScore: undefined,
|
||||
secondaryScore: []
|
||||
};
|
||||
}
|
||||
|
||||
// rrf -> rerank -> embedding -> fullText 优先级
|
||||
let rrfScore: ScoreItemType | undefined = undefined;
|
||||
let reRankScore: ScoreItemType | undefined = undefined;
|
||||
let embeddingScore: ScoreItemType | undefined = undefined;
|
||||
let fullTextScore: ScoreItemType | undefined = undefined;
|
||||
|
||||
score.forEach((item) => {
|
||||
if (item.type === SearchScoreTypeEnum.rrf) {
|
||||
rrfScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.reRank) {
|
||||
reRankScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.embedding) {
|
||||
embeddingScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.fullText) {
|
||||
fullTextScore = item;
|
||||
}
|
||||
});
|
||||
|
||||
const primaryScore = (rrfScore ||
|
||||
reRankScore ||
|
||||
embeddingScore ||
|
||||
fullTextScore) as unknown as ScoreItemType;
|
||||
const secondaryScore = [rrfScore, reRankScore, embeddingScore, fullTextScore].filter(
|
||||
// @ts-ignore
|
||||
(item) => item && primaryScore && item.type !== primaryScore.type
|
||||
) as unknown as ScoreItemType[];
|
||||
|
||||
return {
|
||||
primaryScore,
|
||||
secondaryScore
|
||||
};
|
||||
};
|
||||
|
||||
const QuoteItem = ({
|
||||
quoteItem,
|
||||
canViewSource,
|
||||
@@ -58,44 +99,7 @@ const QuoteItem = ({
|
||||
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
|
||||
|
||||
const score = useMemo(() => {
|
||||
if (!Array.isArray(quoteItem.score)) {
|
||||
return {
|
||||
primaryScore: undefined,
|
||||
secondaryScore: []
|
||||
};
|
||||
}
|
||||
|
||||
// rrf -> rerank -> embedding -> fullText 优先级
|
||||
let rrfScore: ScoreItemType | undefined = undefined;
|
||||
let reRankScore: ScoreItemType | undefined = undefined;
|
||||
let embeddingScore: ScoreItemType | undefined = undefined;
|
||||
let fullTextScore: ScoreItemType | undefined = undefined;
|
||||
|
||||
quoteItem.score.forEach((item) => {
|
||||
if (item.type === SearchScoreTypeEnum.rrf) {
|
||||
rrfScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.reRank) {
|
||||
reRankScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.embedding) {
|
||||
embeddingScore = item;
|
||||
} else if (item.type === SearchScoreTypeEnum.fullText) {
|
||||
fullTextScore = item;
|
||||
}
|
||||
});
|
||||
|
||||
const primaryScore = (rrfScore ||
|
||||
reRankScore ||
|
||||
embeddingScore ||
|
||||
fullTextScore) as unknown as ScoreItemType;
|
||||
const secondaryScore = [rrfScore, reRankScore, embeddingScore, fullTextScore].filter(
|
||||
// @ts-ignore
|
||||
(item) => item && primaryScore && item.type !== primaryScore.type
|
||||
) as unknown as ScoreItemType[];
|
||||
|
||||
return {
|
||||
primaryScore,
|
||||
secondaryScore
|
||||
};
|
||||
return formatScore(quoteItem.score);
|
||||
}, [quoteItem.score]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,5 +39,6 @@ export type DatasetDataListItemType = {
|
||||
q: string; // embedding content
|
||||
a: string; // bonus content
|
||||
chunkIndex?: number;
|
||||
updated?: boolean;
|
||||
// indexes: DatasetDataSchemaType['indexes'];
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ import ChatRecordContextProvider, {
|
||||
} from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
|
||||
const ChatBox = dynamic(() => import('@/components/core/chat/ChatContainer/ChatBox'));
|
||||
@@ -37,6 +38,8 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
|
||||
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@@ -76,7 +79,7 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
zIndex={3}
|
||||
position={['fixed', 'absolute']}
|
||||
top={[0, '2%']}
|
||||
right={0}
|
||||
right={quoteData ? 600 : 0}
|
||||
h={['100%', '96%']}
|
||||
w={'100%'}
|
||||
maxW={['100%', '600px']}
|
||||
@@ -168,6 +171,26 @@ const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
|
||||
)}
|
||||
</Box>
|
||||
</MyBox>
|
||||
{quoteData && (
|
||||
<Box
|
||||
w={['full', '588px']}
|
||||
zIndex={300}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={'95%'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Box zIndex={2} position={'fixed'} top={0} left={0} bottom={0} right={0} onClick={onClose} />
|
||||
</>
|
||||
);
|
||||
@@ -189,6 +212,7 @@ const Render = (props: Props) => {
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={params}>
|
||||
|
||||
@@ -42,7 +42,6 @@ import { getDocPath } from '@/web/common/system/doc';
|
||||
import dynamic from 'next/dynamic';
|
||||
import MyMenu from '@fastgpt/web/components/common/MyMenu';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
|
||||
@@ -185,6 +184,7 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
|
||||
name: item.name,
|
||||
responseDetail: item.responseDetail ?? false,
|
||||
showRawSource: item.showRawSource ?? false,
|
||||
// showFullText: item.showFullText ?? false,
|
||||
showNodeStatus: item.showNodeStatus ?? false,
|
||||
limit: item.limit
|
||||
})
|
||||
@@ -270,7 +270,6 @@ function EditLinkModal({
|
||||
}) {
|
||||
const { feConfigs } = useSystemStore();
|
||||
const { t } = useTranslation();
|
||||
const { publishT } = useI18n();
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
@@ -281,6 +280,7 @@ function EditLinkModal({
|
||||
});
|
||||
|
||||
const responseDetail = watch('responseDetail');
|
||||
// const showFullText = watch('showFullText');
|
||||
const showRawSource = watch('showRawSource');
|
||||
|
||||
const isEdit = useMemo(() => !!defaultData._id, [defaultData]);
|
||||
@@ -306,7 +306,7 @@ function EditLinkModal({
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
iconSrc="/imgs/modal/shareFill.svg"
|
||||
title={isEdit ? publishT('edit_link') : publishT('create_link')}
|
||||
title={isEdit ? t('publish:edit_link') : t('publish:create_link')}
|
||||
maxW={['90vw', '700px']}
|
||||
w={'100%'}
|
||||
h={['90vh', 'auto']}
|
||||
@@ -325,10 +325,10 @@ function EditLinkModal({
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<FormLabel flex={'0 0 90px'}>{t('common:Name')}</FormLabel>
|
||||
<Input
|
||||
placeholder={publishT('link_name')}
|
||||
placeholder={t('publish:link_name')}
|
||||
maxLength={20}
|
||||
{...register('name', {
|
||||
required: t('common:common.name_is_empty') || 'name_is_empty'
|
||||
required: t('common:common.name_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -353,7 +353,7 @@ function EditLinkModal({
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>QPM</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('qpm_tips')}></QuestionTip>
|
||||
<QuestionTip ml={1} label={t('publish:qpm_tips')}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
max={1000}
|
||||
@@ -361,7 +361,7 @@ function EditLinkModal({
|
||||
min: 0,
|
||||
max: 1000,
|
||||
valueAsNumber: true,
|
||||
required: publishT('qpm_is_empty') || ''
|
||||
required: t('publish:qpm_is_empty')
|
||||
})}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -385,11 +385,11 @@ function EditLinkModal({
|
||||
|
||||
<Flex alignItems={'center'} mt={4}>
|
||||
<Flex flex={'0 0 90px'} alignItems={'center'}>
|
||||
<FormLabel>{publishT('token_auth')}</FormLabel>
|
||||
<QuestionTip ml={1} label={publishT('token_auth_tips') || ''}></QuestionTip>
|
||||
<FormLabel>{t('publish:token_auth')}</FormLabel>
|
||||
<QuestionTip ml={1} label={t('publish:token_auth_tips')}></QuestionTip>
|
||||
</Flex>
|
||||
<Input
|
||||
placeholder={publishT('token_auth_tips') || ''}
|
||||
placeholder={t('publish:token_auth_tips')}
|
||||
fontSize={'sm'}
|
||||
{...register('limit.hookUrl')}
|
||||
/>
|
||||
@@ -400,7 +400,7 @@ function EditLinkModal({
|
||||
fontSize={'xs'}
|
||||
color={'myGray.500'}
|
||||
>
|
||||
{publishT('token_auth_use_cases')}
|
||||
{t('publish:token_auth_use_cases')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
@@ -421,8 +421,39 @@ function EditLinkModal({
|
||||
label={t('common:support.outlink.share.Response Quote tips')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Switch {...register('responseDetail')} isChecked={responseDetail} />
|
||||
<Switch
|
||||
{...register('responseDetail', {
|
||||
onChange(e) {
|
||||
if (!e.target.checked) {
|
||||
// setValue('showFullText', false);
|
||||
setValue('showRawSource', false);
|
||||
}
|
||||
}
|
||||
})}
|
||||
isChecked={responseDetail}
|
||||
/>
|
||||
</Flex>
|
||||
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.Chat_quote_reader')}</FormLabel>
|
||||
<QuestionTip
|
||||
ml={1}
|
||||
label={t('common:support.outlink.share.Full_text tips')}
|
||||
></QuestionTip>
|
||||
</Flex>
|
||||
<Switch
|
||||
{...register('showFullText', {
|
||||
onChange(e) {
|
||||
if (e.target.checked) {
|
||||
setValue('responseDetail', true);
|
||||
} else {
|
||||
setValue('showRawSource', false);
|
||||
}
|
||||
}
|
||||
})}
|
||||
isChecked={showFullText}
|
||||
/>
|
||||
</Flex> */}
|
||||
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
|
||||
<Flex alignItems={'center'}>
|
||||
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
|
||||
@@ -436,6 +467,7 @@ function EditLinkModal({
|
||||
onChange(e) {
|
||||
if (e.target.checked) {
|
||||
setValue('responseDetail', true);
|
||||
// setValue('showFullText', true);
|
||||
}
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -7,21 +7,26 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSafeState } from 'ahooks';
|
||||
import { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
|
||||
import { form2AppWorkflow } from '@/web/core/app/utils';
|
||||
import { useI18n } from '@/web/context/I18n';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { AppContext } from '../context';
|
||||
import { useChatTest } from '../useChatTest';
|
||||
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
|
||||
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import { cardStyles } from '../constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
type Props = { appForm: AppSimpleEditFormType };
|
||||
const ChatTest = ({ appForm }: Props) => {
|
||||
type Props = {
|
||||
appForm: AppSimpleEditFormType;
|
||||
setRenderEdit: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
const ChatTest = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { appT } = useI18n();
|
||||
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const [workflowData, setWorkflowData] = useSafeState({
|
||||
nodes: appDetail.modules || [],
|
||||
@@ -32,6 +37,11 @@ const ChatTest = ({ appForm }: Props) => {
|
||||
const { nodes, edges } = form2AppWorkflow(appForm, t);
|
||||
setWorkflowData({ nodes, edges });
|
||||
}, [appForm, setWorkflowData, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderEdit(!quoteData);
|
||||
}, [quoteData, setRenderEdit]);
|
||||
|
||||
const { ChatContainer, restartChat, loading } = useChatTest({
|
||||
...workflowData,
|
||||
chatConfig: appForm.chatConfig,
|
||||
@@ -39,41 +49,56 @@ const ChatTest = ({ appForm }: Props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'100%'}
|
||||
py={4}
|
||||
>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
|
||||
{appT('chat_debug')}
|
||||
<Flex h={'full'} gap={2}>
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
display={'flex'}
|
||||
position={'relative'}
|
||||
flexDirection={'column'}
|
||||
h={'full'}
|
||||
w={quoteData ? '' : 'full'}
|
||||
py={4}
|
||||
{...cardStyles}
|
||||
boxShadow={'3'}
|
||||
>
|
||||
<Flex px={[2, 5]}>
|
||||
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
|
||||
{t('app:chat_debug')}
|
||||
</Box>
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
restartChat();
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
<MyTooltip label={t('common:core.chat.Restart')}>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'smSquare'}
|
||||
icon={<MyIcon name={'common/clearLight'} w={'14px'} />}
|
||||
variant={'whiteDanger'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
restartChat();
|
||||
}}
|
||||
</MyBox>
|
||||
{quoteData && (
|
||||
<Box w={['full', '588px']} {...cardStyles} boxShadow={'3'}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box flex={1}>
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
</MyBox>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const Render = ({ appForm }: Props) => {
|
||||
const Render = ({ appForm, setRenderEdit }: Props) => {
|
||||
const { chatId } = useChatStore();
|
||||
const { appDetail } = useContextSelector(AppContext, (v) => v);
|
||||
|
||||
@@ -90,10 +115,11 @@ const Render = ({ appForm }: Props) => {
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
<ChatTest appForm={appForm} />
|
||||
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
|
||||
</ChatRecordContextProvider>
|
||||
</ChatItemContextProvider>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
|
||||
import ChatTest from './ChatTest';
|
||||
@@ -21,6 +21,7 @@ const Edit = ({
|
||||
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const [renderEdit, setRenderEdit] = useState(true);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -32,24 +33,26 @@ const Edit = ({
|
||||
borderRadius={'lg'}
|
||||
overflowY={['auto', 'unset']}
|
||||
>
|
||||
<Box
|
||||
className={styles.EditAppBox}
|
||||
pr={[0, 1]}
|
||||
overflowY={'auto'}
|
||||
minW={['auto', '580px']}
|
||||
flex={'1'}
|
||||
>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard appForm={appForm} setPast={setPast} />
|
||||
</Box>
|
||||
{renderEdit && (
|
||||
<Box
|
||||
className={styles.EditAppBox}
|
||||
pr={[0, 1]}
|
||||
overflowY={'auto'}
|
||||
minW={['auto', '580px']}
|
||||
flex={'1'}
|
||||
>
|
||||
<Box {...cardStyles} boxShadow={'2'}>
|
||||
<AppCard appForm={appForm} setPast={setPast} />
|
||||
</Box>
|
||||
|
||||
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
|
||||
<EditForm appForm={appForm} setAppForm={setAppForm} />
|
||||
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>
|
||||
<EditForm appForm={appForm} setAppForm={setAppForm} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{isPc && (
|
||||
<Box {...cardStyles} boxShadow={'3'} flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest appForm={appForm} />
|
||||
<Box flex={'2 0 0'} w={0} mb={3}>
|
||||
<ChatTest appForm={appForm} setRenderEdit={setRenderEdit} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -20,6 +20,7 @@ import ChatRecordContextProvider, {
|
||||
} from '@/web/core/chat/context/chatRecordContext';
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
@@ -41,10 +42,13 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
});
|
||||
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
|
||||
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex h={'full'}>
|
||||
<Box
|
||||
zIndex={300}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
@@ -53,7 +57,10 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
left={0}
|
||||
bottom={0}
|
||||
right={0}
|
||||
onClick={onClose}
|
||||
onClick={() => {
|
||||
setQuoteData(undefined);
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
<MyBox
|
||||
isLoading={loading}
|
||||
@@ -62,7 +69,7 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
right={quoteData ? 600 : 0}
|
||||
h={isOpen ? '95%' : '0'}
|
||||
w={isOpen ? ['100%', '460px'] : '0'}
|
||||
bg={'white'}
|
||||
@@ -141,7 +148,27 @@ const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
|
||||
<ChatContainer />
|
||||
</Box>
|
||||
</MyBox>
|
||||
</>
|
||||
{quoteData && (
|
||||
<Box
|
||||
w={['full', '588px']}
|
||||
zIndex={300}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={'95%'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -162,6 +189,7 @@ const Render = (Props: Props) => {
|
||||
showRouteToAppDetail={true}
|
||||
showRouteToDatasetDetail={true}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
|
||||
@@ -46,6 +46,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
const appName = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.name);
|
||||
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app.avatar);
|
||||
const showRouteToAppDetail = useContextSelector(ChatItemContext, (v) => v.showRouteToAppDetail);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const concatHistory = useMemo(() => {
|
||||
const formatHistories: HistoryItemType[] = histories.map((item) => {
|
||||
@@ -144,7 +145,10 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
borderRadius={'xl'}
|
||||
leftIcon={<MyIcon name={'core/chat/chatLight'} w={'16px'} />}
|
||||
overflow={'hidden'}
|
||||
onClick={() => onChangeChatId()}
|
||||
onClick={() => {
|
||||
onChangeChatId();
|
||||
setQuoteData(undefined);
|
||||
}}
|
||||
>
|
||||
{t('common:core.chat.New Chat')}
|
||||
</Button>
|
||||
@@ -199,6 +203,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
: {
|
||||
onClick: () => {
|
||||
onChangeChatId(item.id);
|
||||
setQuoteData(undefined);
|
||||
}
|
||||
})}
|
||||
{...(i !== concatHistory.length - 1 && {
|
||||
@@ -270,6 +275,7 @@ const ChatHistorySlider = ({ confirmClearText }: { confirmClearText: string }) =
|
||||
onDelHistory(item.id);
|
||||
if (item.id === activeChatId) {
|
||||
onChangeChatId();
|
||||
setQuoteData(undefined);
|
||||
}
|
||||
},
|
||||
type: 'danger'
|
||||
|
||||
@@ -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;
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
delOneDatasetDataById,
|
||||
getDatasetCollectionById
|
||||
} from '@/web/core/dataset/api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
|
||||
@@ -178,7 +178,6 @@ const InputDataModal = ({
|
||||
async (e: InputDataType) => {
|
||||
if (!dataId) return Promise.reject(t('common:common.error.unKnow'));
|
||||
|
||||
// not exactly same
|
||||
await putDatasetDataById({
|
||||
dataId,
|
||||
q: e.q,
|
||||
|
||||
250
projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts
Normal file
250
projects/app/src/pages/api/core/chat/quote/getCollectionQuote.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
|
||||
import { DatasetDataSchemaType } from '@fastgpt/global/core/dataset/type';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { LinkedListResponse, LinkedPaginationProps } from '@fastgpt/web/common/fetch/type';
|
||||
import { FilterQuery, Types } from 'mongoose';
|
||||
import { dataFieldSelector, processChatTimeFilter } from './getQuote';
|
||||
|
||||
export type GetCollectionQuoteProps = LinkedPaginationProps & {
|
||||
chatTime: Date;
|
||||
|
||||
isInitialLoad: boolean;
|
||||
collectionId: string;
|
||||
chatItemId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
teamId?: string;
|
||||
teamToken?: string;
|
||||
};
|
||||
|
||||
export type GetCollectionQuoteRes = LinkedListResponse<DatasetDataSchemaType>;
|
||||
|
||||
type BaseMatchType = FilterQuery<DatasetDataSchemaType>;
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<GetCollectionQuoteProps>
|
||||
): Promise<GetCollectionQuoteRes> {
|
||||
const {
|
||||
initialId,
|
||||
initialIndex,
|
||||
prevId,
|
||||
prevIndex,
|
||||
nextId,
|
||||
nextIndex,
|
||||
chatTime,
|
||||
|
||||
isInitialLoad,
|
||||
collectionId,
|
||||
chatItemId,
|
||||
appId,
|
||||
chatId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken,
|
||||
pageSize = 15
|
||||
} = req.body;
|
||||
const limitedPageSize = Math.min(pageSize, 30);
|
||||
|
||||
await Promise.all([
|
||||
authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
appId,
|
||||
chatId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
}),
|
||||
authCollectionInChat({ appId, chatId, chatItemId, collectionId })
|
||||
]);
|
||||
|
||||
const baseMatch: BaseMatchType = {
|
||||
collectionId,
|
||||
$or: [
|
||||
{ updateTime: { $lt: new Date(chatTime) } },
|
||||
{ history: { $elemMatch: { updateTime: { $lt: new Date(chatTime) } } } }
|
||||
]
|
||||
};
|
||||
|
||||
if (initialId && initialIndex !== undefined) {
|
||||
return await handleInitialLoad(
|
||||
initialId,
|
||||
initialIndex,
|
||||
limitedPageSize,
|
||||
chatTime,
|
||||
chatItemId,
|
||||
isInitialLoad,
|
||||
baseMatch
|
||||
);
|
||||
}
|
||||
|
||||
if ((prevId && prevIndex !== undefined) || (nextId && nextIndex !== undefined)) {
|
||||
return await handlePaginatedLoad(
|
||||
prevId,
|
||||
prevIndex,
|
||||
nextId,
|
||||
nextIndex,
|
||||
limitedPageSize,
|
||||
chatTime,
|
||||
chatItemId,
|
||||
baseMatch
|
||||
);
|
||||
}
|
||||
|
||||
return { list: [], hasMorePrev: false, hasMoreNext: false };
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
async function handleInitialLoad(
|
||||
initialId: string,
|
||||
initialIndex: number,
|
||||
pageSize: number,
|
||||
chatTime: Date,
|
||||
chatItemId: string,
|
||||
isInitialLoad: boolean,
|
||||
baseMatch: BaseMatchType
|
||||
): Promise<GetCollectionQuoteRes> {
|
||||
const centerNode = await MongoDatasetData.findOne(
|
||||
{
|
||||
_id: new Types.ObjectId(initialId)
|
||||
},
|
||||
dataFieldSelector
|
||||
).lean();
|
||||
|
||||
if (!centerNode) {
|
||||
if (isInitialLoad) {
|
||||
const list = await MongoDatasetData.find(baseMatch, dataFieldSelector)
|
||||
.sort({ chunkIndex: 1, _id: -1 })
|
||||
.limit(pageSize)
|
||||
.lean();
|
||||
|
||||
const listRes = list.map((item, index) => ({
|
||||
...item,
|
||||
index: item.chunkIndex
|
||||
}));
|
||||
|
||||
const hasMoreNext = list.length === pageSize;
|
||||
|
||||
return {
|
||||
list: listRes,
|
||||
hasMorePrev: false,
|
||||
hasMoreNext
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject('centerNode not found');
|
||||
}
|
||||
|
||||
const prevHalfSize = Math.floor(pageSize / 2);
|
||||
const nextHalfSize = pageSize - prevHalfSize - 1;
|
||||
|
||||
const { list: prevList, hasMore: hasMorePrev } = await getPrevNodes(
|
||||
initialId,
|
||||
initialIndex,
|
||||
prevHalfSize,
|
||||
baseMatch
|
||||
);
|
||||
const { list: nextList, hasMore: hasMoreNext } = await getNextNodes(
|
||||
initialId,
|
||||
initialIndex,
|
||||
nextHalfSize,
|
||||
baseMatch
|
||||
);
|
||||
|
||||
const resultList = [...prevList, centerNode, ...nextList];
|
||||
|
||||
const list = processChatTimeFilter(resultList, chatTime);
|
||||
|
||||
return {
|
||||
list: list.map((item) => ({
|
||||
...item,
|
||||
index: item.chunkIndex
|
||||
})),
|
||||
hasMorePrev,
|
||||
hasMoreNext
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePaginatedLoad(
|
||||
prevId: string | undefined,
|
||||
prevIndex: number | undefined,
|
||||
nextId: string | undefined,
|
||||
nextIndex: number | undefined,
|
||||
pageSize: number,
|
||||
chatTime: Date,
|
||||
chatItemId: string,
|
||||
baseMatch: BaseMatchType
|
||||
): Promise<GetCollectionQuoteRes> {
|
||||
const { list, hasMore } =
|
||||
prevId && prevIndex !== undefined
|
||||
? await getPrevNodes(prevId, prevIndex, pageSize, baseMatch)
|
||||
: await getNextNodes(nextId!, nextIndex!, pageSize, baseMatch);
|
||||
|
||||
const processedList = processChatTimeFilter(list, chatTime);
|
||||
|
||||
return {
|
||||
list: processedList.map((item) => ({
|
||||
...item,
|
||||
index: item.chunkIndex
|
||||
})),
|
||||
hasMorePrev: !!prevId && hasMore,
|
||||
hasMoreNext: !!nextId && hasMore
|
||||
};
|
||||
}
|
||||
|
||||
async function getPrevNodes(
|
||||
initialId: string,
|
||||
initialIndex: number,
|
||||
limit: number,
|
||||
baseMatch: BaseMatchType
|
||||
) {
|
||||
const match: BaseMatchType = {
|
||||
...baseMatch,
|
||||
$or: [
|
||||
{ chunkIndex: { $lte: initialIndex } },
|
||||
{ chunkIndex: initialIndex, _id: { $lte: new Types.ObjectId(initialId) } }
|
||||
]
|
||||
};
|
||||
|
||||
const list = await MongoDatasetData.find(match, dataFieldSelector)
|
||||
.sort({ chunkIndex: -1, _id: 1 })
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
return {
|
||||
list: list.filter((item) => String(item._id) !== initialId).reverse(),
|
||||
hasMore: list.length === limit
|
||||
};
|
||||
}
|
||||
|
||||
async function getNextNodes(
|
||||
initialId: string,
|
||||
initialIndex: number,
|
||||
limit: number,
|
||||
baseMatch: BaseMatchType
|
||||
) {
|
||||
const match: BaseMatchType = {
|
||||
...baseMatch,
|
||||
$or: [
|
||||
{ chunkIndex: { $gte: initialIndex } },
|
||||
{ chunkIndex: initialIndex, _id: { $gte: new Types.ObjectId(initialId) } }
|
||||
]
|
||||
};
|
||||
|
||||
const list = await MongoDatasetData.find(match, dataFieldSelector)
|
||||
.sort({ chunkIndex: 1, _id: -1 })
|
||||
.limit(limit)
|
||||
.lean();
|
||||
|
||||
return {
|
||||
list: list.filter((item) => String(item._id) !== initialId),
|
||||
hasMore: list.length === limit
|
||||
};
|
||||
}
|
||||
102
projects/app/src/pages/api/core/chat/quote/getQuote.ts
Normal file
102
projects/app/src/pages/api/core/chat/quote/getQuote.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
|
||||
import { DatasetDataSchemaType } from '@fastgpt/global/core/dataset/type';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
|
||||
export type GetQuoteDataProps = {
|
||||
datasetDataIdList: string[];
|
||||
chatTime: Date;
|
||||
|
||||
collectionIdList: string[];
|
||||
chatItemId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
shareId?: string;
|
||||
outLinkUid?: string;
|
||||
teamId?: string;
|
||||
teamToken?: string;
|
||||
};
|
||||
|
||||
export type GetQuoteDataRes = {
|
||||
quoteList: DatasetDataSchemaType[];
|
||||
};
|
||||
|
||||
export const dataFieldSelector =
|
||||
'_id datasetId collectionId q a chunkIndex history updateTime currentChatItemId prevId';
|
||||
|
||||
async function handler(req: ApiRequestProps<GetQuoteDataProps>): Promise<GetQuoteDataRes> {
|
||||
const {
|
||||
datasetDataIdList,
|
||||
chatTime,
|
||||
|
||||
collectionIdList,
|
||||
chatItemId,
|
||||
chatId,
|
||||
appId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
} = req.body;
|
||||
|
||||
await authChatCrud({
|
||||
req,
|
||||
authToken: true,
|
||||
appId,
|
||||
chatId,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
collectionIdList.map(async (collectionId) => {
|
||||
await authCollectionInChat({ appId, chatId, chatItemId, collectionId });
|
||||
})
|
||||
);
|
||||
|
||||
const list = await MongoDatasetData.find(
|
||||
{ _id: { $in: datasetDataIdList } },
|
||||
dataFieldSelector
|
||||
).lean();
|
||||
|
||||
const quoteList = processChatTimeFilter(list, chatTime);
|
||||
|
||||
return {
|
||||
quoteList
|
||||
};
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export function processChatTimeFilter(list: DatasetDataSchemaType[], chatTime?: Date) {
|
||||
if (!chatTime) return list;
|
||||
|
||||
return list.map((item) => {
|
||||
if (!item.history) return item;
|
||||
|
||||
const { history, ...rest } = item;
|
||||
const formatedChatTime = new Date(chatTime);
|
||||
|
||||
if (item.updateTime <= formatedChatTime) {
|
||||
return rest;
|
||||
}
|
||||
|
||||
const latestHistoryIndex = history.findIndex(
|
||||
(historyItem: any) => historyItem.updateTime <= formatedChatTime
|
||||
);
|
||||
|
||||
if (latestHistoryIndex === -1) return rest;
|
||||
|
||||
const latestHistory = history[latestHistoryIndex];
|
||||
|
||||
return {
|
||||
...rest,
|
||||
q: latestHistory?.q || item.q,
|
||||
a: latestHistory?.a || item.a,
|
||||
updated: true
|
||||
};
|
||||
});
|
||||
}
|
||||
78
projects/app/src/pages/api/core/dataset/collection/export.ts
Normal file
78
projects/app/src/pages/api/core/dataset/collection/export.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { useIPFrequencyLimit } from '@fastgpt/service/common/middle/reqFrequencyLimit';
|
||||
import { readFromSecondary } from '@fastgpt/service/common/mongo/utils';
|
||||
import { responseWriteController } from '@fastgpt/service/common/response';
|
||||
import { addLog } from '@fastgpt/service/common/system/log';
|
||||
import { MongoDatasetData } from '@fastgpt/service/core/dataset/data/schema';
|
||||
import { authDatasetCollection } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { ApiRequestProps } from '@fastgpt/service/type/next';
|
||||
import { NextApiResponse } from 'next';
|
||||
|
||||
export type ExportCollectionBody = {
|
||||
collectionId: string;
|
||||
chatTime: Date;
|
||||
};
|
||||
|
||||
async function handler(req: ApiRequestProps<ExportCollectionBody, {}>, res: NextApiResponse) {
|
||||
let { collectionId, chatTime } = req.body;
|
||||
|
||||
const { teamId, collection } = await authDatasetCollection({
|
||||
req,
|
||||
authToken: true,
|
||||
collectionId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
const where = {
|
||||
teamId,
|
||||
datasetId: collection.datasetId,
|
||||
collectionId,
|
||||
...(chatTime
|
||||
? {
|
||||
$or: [
|
||||
{ updateTime: { $lt: new Date(chatTime) } },
|
||||
{ history: { $elemMatch: { updateTime: { $lt: new Date(chatTime) } } } }
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
|
||||
res.setHeader('Content-Type', 'text/csv; charset=utf-8;');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=usage.csv; ');
|
||||
|
||||
const cursor = MongoDatasetData.find(where, 'q a', {
|
||||
...readFromSecondary,
|
||||
batchSize: 1000
|
||||
})
|
||||
.sort({ chunkIndex: 1 })
|
||||
.limit(50000)
|
||||
.cursor();
|
||||
|
||||
const write = responseWriteController({
|
||||
res,
|
||||
readStream: cursor
|
||||
});
|
||||
|
||||
cursor.on('data', (doc) => {
|
||||
const res = doc.a ? `\n${doc.q}\n${doc.a}` : `\n${doc.q}`;
|
||||
|
||||
write(res);
|
||||
});
|
||||
|
||||
cursor.on('end', () => {
|
||||
cursor.close();
|
||||
res.end();
|
||||
});
|
||||
|
||||
cursor.on('error', (err) => {
|
||||
addLog.error(`export usage error`, err);
|
||||
res.status(500);
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
|
||||
export default NextAPI(
|
||||
useIPFrequencyLimit({ id: 'export-usage', seconds: 60, limit: 1, force: true }),
|
||||
handler
|
||||
);
|
||||
@@ -7,9 +7,7 @@ import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/con
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { AIChatItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||
import { authChatCrud } from '@/service/support/permission/auth/chat';
|
||||
import { authChatCrud, authCollectionInChat } from '@/service/support/permission/auth/chat';
|
||||
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
|
||||
import { useApiDatasetRequest } from '@fastgpt/service/core/dataset/apiDataset/api';
|
||||
import { POST } from '@fastgpt/service/common/api/plusRequest';
|
||||
@@ -29,57 +27,6 @@ export type readCollectionSourceResponse = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const authCollectionInChat = async ({
|
||||
collectionId,
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId
|
||||
}: {
|
||||
collectionId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
chatItemId: string;
|
||||
}) => {
|
||||
try {
|
||||
const chatItem = (await MongoChatItem.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId,
|
||||
dataId: chatItemId
|
||||
},
|
||||
'responseData'
|
||||
).lean()) as AIChatItemType;
|
||||
|
||||
if (!chatItem) return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
|
||||
// 找 responseData 里,是否有该文档 id
|
||||
const responseData = chatItem.responseData || [];
|
||||
const flatResData: ChatHistoryItemResType[] =
|
||||
responseData
|
||||
?.map((item) => {
|
||||
return [
|
||||
item,
|
||||
...(item.pluginDetail || []),
|
||||
...(item.toolDetail || []),
|
||||
...(item.loopDetail || [])
|
||||
];
|
||||
})
|
||||
.flat() || [];
|
||||
|
||||
if (
|
||||
flatResData.some((item) => {
|
||||
if (item.quoteList) {
|
||||
return item.quoteList.some((quote) => quote.collectionId === collectionId);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
};
|
||||
|
||||
async function handler(
|
||||
req: ApiRequestProps<readCollectionSourceBody, readCollectionSourceQuery>
|
||||
): Promise<readCollectionSourceResponse> {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { NextApiRequest } from 'next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
|
||||
export type GetQuotePermissionResponse =
|
||||
| {
|
||||
permission: {
|
||||
hasWritePer: boolean;
|
||||
hasReadPer: boolean;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
async function handler(req: NextApiRequest): Promise<GetQuotePermissionResponse> {
|
||||
const { id: datasetId } = req.query as {
|
||||
id?: string;
|
||||
};
|
||||
if (!datasetId) {
|
||||
return Promise.reject('datasetId is required');
|
||||
}
|
||||
|
||||
try {
|
||||
const { permission } = await authDataset({
|
||||
req,
|
||||
authToken: true,
|
||||
authApiKey: true,
|
||||
datasetId,
|
||||
per: ReadPermissionVal
|
||||
});
|
||||
|
||||
return {
|
||||
permission: {
|
||||
hasReadPer: permission.hasReadPer,
|
||||
hasWritePer: permission.hasWritePer
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error === DatasetErrEnum.unAuthDataset) {
|
||||
return {
|
||||
permission: {
|
||||
hasWritePer: false,
|
||||
hasReadPer: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
@@ -45,7 +45,7 @@ async function handler(
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
MongoDatasetData.find(match, '_id datasetId collectionId q a chunkIndex')
|
||||
.sort({ chunkIndex: 1, updateTime: -1 })
|
||||
.sort({ chunkIndex: 1, _id: -1 })
|
||||
.skip(offset)
|
||||
.limit(pageSize)
|
||||
.lean(),
|
||||
|
||||
@@ -37,6 +37,7 @@ async function handler(
|
||||
responseDetail,
|
||||
showRawSource,
|
||||
showNodeStatus,
|
||||
// showFullText,
|
||||
limit,
|
||||
app
|
||||
});
|
||||
|
||||
@@ -59,6 +59,7 @@ import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatc
|
||||
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
|
||||
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
|
||||
import { ExternalProviderType } from '@fastgpt/global/core/workflow/runtime/type';
|
||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||
|
||||
type FastGptWebChatProps = {
|
||||
chatId?: string; // undefined: get histories from messages, '': new chat, 'xxxxx': get histories from db
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
import NextHead from '@/components/common/NextHead';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getInitChatInfo } from '@/web/core/chat/api';
|
||||
@@ -36,6 +36,7 @@ import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/contex
|
||||
import ChatRecordContextProvider, {
|
||||
ChatRecordContext
|
||||
} from '@/web/core/chat/context/chatRecordContext';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
|
||||
|
||||
@@ -58,6 +59,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
|
||||
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@@ -138,13 +141,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
},
|
||||
[appId, chatId, onUpdateHistoryTitle, setChatBoxData, forbidLoadChat]
|
||||
);
|
||||
|
||||
const RenderHistorySlider = useMemo(() => {
|
||||
const Children = (
|
||||
<ChatHistorySlider confirmClearText={t('common:core.chat.Confirm to clear history')} />
|
||||
);
|
||||
|
||||
return isPc || !appId ? (
|
||||
<SideBar>{Children}</SideBar>
|
||||
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
|
||||
) : (
|
||||
<Drawer
|
||||
isOpen={isOpenSlider}
|
||||
@@ -157,7 +161,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}, [appId, isOpenSlider, isPc, onCloseSlider, t]);
|
||||
}, [t, isPc, appId, isOpenSlider, onCloseSlider, quoteData]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'}>
|
||||
@@ -169,7 +173,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<PageContainer isLoading={loading} flex={'1 0 0'} w={0} p={[0, '16px']} position={'relative'}>
|
||||
<PageContainer
|
||||
isLoading={loading}
|
||||
flex={'1 0 0'}
|
||||
w={0}
|
||||
p={[0, '16px']}
|
||||
pr={quoteData ? '8px !important' : '16px'}
|
||||
position={'relative'}
|
||||
>
|
||||
<Flex h={'100%'} flexDirection={['column', 'row']}>
|
||||
{/* pc always show history. */}
|
||||
{RenderHistorySlider}
|
||||
@@ -215,6 +226,16 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
{quoteData && (
|
||||
<PageContainer w={['full', '588px']} insertProps={{ bg: 'white' }}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -278,6 +299,7 @@ const Render = (props: { appId: string; isStandalone?: string }) => {
|
||||
showRouteToAppDetail={isStandalone !== '1'}
|
||||
showRouteToDatasetDetail={isStandalone !== '1'}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
|
||||
@@ -37,6 +37,7 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
|
||||
import { AppSchema } from '@fastgpt/global/core/app/type';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
|
||||
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
|
||||
|
||||
@@ -49,6 +50,7 @@ type Props = {
|
||||
authToken: string;
|
||||
customUid: string;
|
||||
showRawSource: boolean;
|
||||
// showFullText: boolean;
|
||||
showNodeStatus: boolean;
|
||||
};
|
||||
|
||||
@@ -81,6 +83,8 @@ const OutLink = (props: Props) => {
|
||||
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
|
||||
const isPlugin = useContextSelector(ChatItemContext, (v) => v.isPlugin);
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@@ -217,7 +221,7 @@ const OutLink = (props: Props) => {
|
||||
if (showHistory !== '1') return null;
|
||||
|
||||
return isPc ? (
|
||||
<SideBar>{Children}</SideBar>
|
||||
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
|
||||
) : (
|
||||
<Drawer
|
||||
isOpen={isOpenSlider}
|
||||
@@ -232,10 +236,10 @@ const OutLink = (props: Props) => {
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}, [isOpenSlider, isPc, onCloseSlider, showHistory, t]);
|
||||
}, [isOpenSlider, isPc, onCloseSlider, quoteData, showHistory, t]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box h={'full'} display={quoteData ? 'flex' : ''}>
|
||||
<NextHead
|
||||
title={props.appName || data?.app?.name || 'AI'}
|
||||
desc={props.appIntro || data?.app?.intro}
|
||||
@@ -291,7 +295,17 @@ const OutLink = (props: Props) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
</>
|
||||
{quoteData && (
|
||||
<PageContainer w={['full', '800px']} py={5}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -340,6 +354,7 @@ const Render = (props: Props) => {
|
||||
showRouteToAppDetail={false}
|
||||
showRouteToDatasetDetail={false}
|
||||
isShowReadRawSource={props.showRawSource}
|
||||
// isShowFullText={props.showFullText}
|
||||
showNodeStatus={props.showNodeStatus}
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
@@ -383,6 +398,7 @@ export async function getServerSideProps(context: any) {
|
||||
appAvatar: app?.associatedApp?.avatar ?? '',
|
||||
appIntro: app?.associatedApp?.intro ?? 'AI',
|
||||
showRawSource: app?.showRawSource ?? false,
|
||||
// showFullText: app?.showFullText ?? false,
|
||||
showNodeStatus: app?.showNodeStatus ?? false,
|
||||
shareId: shareId ?? '',
|
||||
authToken: authToken ?? '',
|
||||
|
||||
@@ -33,6 +33,7 @@ import ChatRecordContextProvider, {
|
||||
import { useChatStore } from '@/web/core/chat/context/useChatStore';
|
||||
import { useMount } from 'ahooks';
|
||||
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
|
||||
const CustomPluginRunBox = dynamic(() => import('@/pageComponents/chat/CustomPluginRunBox'));
|
||||
|
||||
type Props = { appId: string; chatId: string; teamId: string; teamToken: string };
|
||||
@@ -63,6 +64,8 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
|
||||
const chatBoxData = useContextSelector(ChatItemContext, (v) => v.chatBoxData);
|
||||
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
|
||||
const quoteData = useContextSelector(ChatItemContext, (v) => v.quoteData);
|
||||
const setQuoteData = useContextSelector(ChatItemContext, (v) => v.setQuoteData);
|
||||
|
||||
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
|
||||
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
|
||||
@@ -163,7 +166,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
);
|
||||
|
||||
return isPc || !appId ? (
|
||||
<SideBar>{Children}</SideBar>
|
||||
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar>
|
||||
) : (
|
||||
<Drawer
|
||||
isOpen={isOpenSlider}
|
||||
@@ -176,7 +179,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}, [appId, isOpenSlider, isPc, onCloseSlider, t]);
|
||||
}, [appId, isOpenSlider, isPc, onCloseSlider, quoteData, t]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'}>
|
||||
@@ -231,6 +234,17 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
|
||||
</Flex>
|
||||
</Flex>
|
||||
</PageContainer>
|
||||
|
||||
{quoteData && (
|
||||
<PageContainer w={['full', '800px']} py={5}>
|
||||
<ChatQuoteList
|
||||
chatTime={quoteData.chatTime}
|
||||
rawSearch={quoteData.rawSearch}
|
||||
metadata={quoteData.metadata}
|
||||
onClose={() => setQuoteData(undefined)}
|
||||
/>
|
||||
</PageContainer>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -300,6 +314,7 @@ const Render = (props: Props) => {
|
||||
showRouteToAppDetail={false}
|
||||
showRouteToDatasetDetail={false}
|
||||
isShowReadRawSource={true}
|
||||
// isShowFullText={true}
|
||||
showNodeStatus
|
||||
>
|
||||
<ChatRecordContextProvider params={chatRecordProviderParams}>
|
||||
|
||||
@@ -220,11 +220,7 @@ export async function updateData2Dataset({
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Update mongo updateTime(便于脏数据检查器识别)
|
||||
mongoData.updateTime = new Date();
|
||||
await mongoData.save();
|
||||
|
||||
// 5. Insert vector
|
||||
// insert vector
|
||||
const insertResult = await Promise.all(
|
||||
patchResult
|
||||
.filter((item) => item.type === 'create' || item.type === 'update')
|
||||
@@ -249,9 +245,19 @@ export async function updateData2Dataset({
|
||||
.filter((item) => item.type !== 'delete')
|
||||
.map((item) => item.index) as DatasetDataIndexItemType[];
|
||||
|
||||
// console.log(clonePatchResult2Insert);
|
||||
await mongoSessionRun(async (session) => {
|
||||
// Update MongoData
|
||||
// update mongo data
|
||||
mongoData.history =
|
||||
q !== mongoData.q || a !== mongoData.a
|
||||
? [
|
||||
{
|
||||
q: mongoData.q,
|
||||
a: mongoData.a,
|
||||
updateTime: mongoData.updateTime
|
||||
},
|
||||
...(mongoData.history?.slice(0, 9) || [])
|
||||
]
|
||||
: mongoData.history;
|
||||
mongoData.q = q || mongoData.q;
|
||||
mongoData.a = a ?? mongoData.a;
|
||||
mongoData.indexes = newIndexes;
|
||||
@@ -277,6 +283,10 @@ export async function updateData2Dataset({
|
||||
}
|
||||
});
|
||||
|
||||
// Update mongo updateTime(便于脏数据检查器识别)
|
||||
mongoData.updateTime = new Date();
|
||||
await mongoData.save();
|
||||
|
||||
return {
|
||||
tokens
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatSchema } from '@fastgpt/global/core/chat/type';
|
||||
import { AIChatItemType, ChatHistoryItemResType, ChatSchema } from '@fastgpt/global/core/chat/type';
|
||||
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
|
||||
import { AuthModeType } from '@fastgpt/service/support/permission/type';
|
||||
import { authOutLink } from './outLink';
|
||||
@@ -6,6 +6,8 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
|
||||
import { authTeamSpaceToken } from './team';
|
||||
import { AuthUserTypeEnum, ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
|
||||
import { authApp } from '@fastgpt/service/support/permission/app/auth';
|
||||
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
|
||||
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
|
||||
|
||||
/*
|
||||
检查chat的权限:
|
||||
@@ -184,3 +186,54 @@ export async function authChatCrud({
|
||||
|
||||
return Promise.reject(ChatErrEnum.unAuthChat);
|
||||
}
|
||||
|
||||
export const authCollectionInChat = async ({
|
||||
collectionId,
|
||||
appId,
|
||||
chatId,
|
||||
chatItemId
|
||||
}: {
|
||||
collectionId: string;
|
||||
appId: string;
|
||||
chatId: string;
|
||||
chatItemId: string;
|
||||
}) => {
|
||||
try {
|
||||
const chatItem = (await MongoChatItem.findOne(
|
||||
{
|
||||
appId,
|
||||
chatId,
|
||||
dataId: chatItemId
|
||||
},
|
||||
'responseData'
|
||||
).lean()) as AIChatItemType;
|
||||
|
||||
if (!chatItem) return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
|
||||
// 找 responseData 里,是否有该文档 id
|
||||
const responseData = chatItem.responseData || [];
|
||||
const flatResData: ChatHistoryItemResType[] =
|
||||
responseData
|
||||
?.map((item) => {
|
||||
return [
|
||||
item,
|
||||
...(item.pluginDetail || []),
|
||||
...(item.toolDetail || []),
|
||||
...(item.loopDetail || [])
|
||||
];
|
||||
})
|
||||
.flat() || [];
|
||||
|
||||
if (
|
||||
flatResData.some((item) => {
|
||||
if (item.quoteList) {
|
||||
return item.quoteList.some((quote) => quote.collectionId === collectionId);
|
||||
}
|
||||
return false;
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {}
|
||||
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ export const defaultOutLinkForm: OutLinkEditType = {
|
||||
name: '',
|
||||
showNodeStatus: true,
|
||||
responseDetail: false,
|
||||
// showFullText: false,
|
||||
showRawSource: false,
|
||||
limit: {
|
||||
QPM: 100,
|
||||
|
||||
@@ -30,6 +30,11 @@ import type {
|
||||
getPaginationRecordsBody,
|
||||
getPaginationRecordsResponse
|
||||
} from '@/pages/api/core/chat/getPaginationRecords';
|
||||
import { GetQuoteDataProps, GetQuoteDataRes } from '@/pages/api/core/chat/quote/getQuote';
|
||||
import {
|
||||
GetCollectionQuoteProps,
|
||||
GetCollectionQuoteRes
|
||||
} from '@/pages/api/core/chat/quote/getCollectionQuote';
|
||||
|
||||
/**
|
||||
* 获取初始化聊天内容
|
||||
@@ -100,3 +105,9 @@ export const getMyTokensApps = (data: AuthTeamTagTokenProps) =>
|
||||
*/
|
||||
export const getinitTeamChat = (data: { teamId: string; authToken: string; appId: string }) =>
|
||||
GET(`/proApi/core/chat/initTeamChat`, data);
|
||||
|
||||
export const getQuoteDataList = (data: GetQuoteDataProps) =>
|
||||
POST<GetQuoteDataRes>(`/core/chat/quote/getQuote`, data);
|
||||
|
||||
export const getCollectionQuote = (data: GetCollectionQuoteProps) =>
|
||||
POST<GetCollectionQuoteRes>(`/core/chat/quote/getCollectionQuote`, data);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChatBoxInputFormType } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
|
||||
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createContext } from 'use-context-selector';
|
||||
import { ComponentRef as ChatComponentRef } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import { useForm, UseFormReturn } from 'react-hook-form';
|
||||
@@ -8,11 +8,13 @@ import { defaultChatData } from '@/global/core/chat/constants';
|
||||
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
|
||||
import { AppChatConfigType, VariableItemType } from '@fastgpt/global/core/app/type';
|
||||
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
|
||||
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
|
||||
|
||||
type ContextProps = {
|
||||
showRouteToAppDetail: boolean;
|
||||
showRouteToDatasetDetail: boolean;
|
||||
isShowReadRawSource: boolean;
|
||||
// isShowFullText: boolean;
|
||||
showNodeStatus: boolean;
|
||||
};
|
||||
type ChatBoxDataType = {
|
||||
@@ -30,6 +32,21 @@ type ChatBoxDataType = {
|
||||
};
|
||||
};
|
||||
|
||||
export type metadataType = {
|
||||
collectionId: string;
|
||||
collectionIdList: string[];
|
||||
chatItemId: string;
|
||||
sourceId: string;
|
||||
sourceName: string;
|
||||
datasetId: string;
|
||||
};
|
||||
|
||||
export type QuoteDataType = {
|
||||
rawSearch: SearchDataResponseItemType[];
|
||||
metadata: metadataType;
|
||||
chatTime: Date;
|
||||
};
|
||||
|
||||
type ChatItemContextType = {
|
||||
ChatBoxRef: React.RefObject<ChatComponentRef> | null;
|
||||
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
|
||||
@@ -43,6 +60,9 @@ type ChatItemContextType = {
|
||||
chatBoxData: ChatBoxDataType;
|
||||
setChatBoxData: React.Dispatch<React.SetStateAction<ChatBoxDataType>>;
|
||||
isPlugin: boolean;
|
||||
|
||||
quoteData?: QuoteDataType;
|
||||
setQuoteData: React.Dispatch<React.SetStateAction<QuoteDataType | undefined>>;
|
||||
} & ContextProps;
|
||||
|
||||
export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
@@ -61,6 +81,11 @@ export const ChatItemContext = createContext<ChatItemContextType>({
|
||||
},
|
||||
clearChatRecords: function (): void {
|
||||
throw new Error('Function not implemented.');
|
||||
},
|
||||
|
||||
quoteData: undefined,
|
||||
setQuoteData: function (value: React.SetStateAction<QuoteDataType | undefined>): void {
|
||||
throw new Error('Function not implemented.');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -72,13 +97,14 @@ const ChatItemContextProvider = ({
|
||||
showRouteToAppDetail,
|
||||
showRouteToDatasetDetail,
|
||||
isShowReadRawSource,
|
||||
// isShowFullText,
|
||||
showNodeStatus
|
||||
}: {
|
||||
children: ReactNode;
|
||||
} & ContextProps) => {
|
||||
const ChatBoxRef = useRef<ChatComponentRef>(null);
|
||||
const variablesForm = useForm<ChatBoxInputFormType>();
|
||||
|
||||
const [quoteData, setQuoteData] = useState<QuoteDataType>();
|
||||
const [chatBoxData, setChatBoxData] = useState<ChatBoxDataType>({
|
||||
...defaultChatData
|
||||
});
|
||||
@@ -131,7 +157,11 @@ const ChatItemContextProvider = ({
|
||||
showRouteToAppDetail,
|
||||
showRouteToDatasetDetail,
|
||||
isShowReadRawSource,
|
||||
showNodeStatus
|
||||
// isShowFullText,
|
||||
showNodeStatus,
|
||||
|
||||
quoteData,
|
||||
setQuoteData
|
||||
};
|
||||
}, [
|
||||
chatBoxData,
|
||||
@@ -143,7 +173,10 @@ const ChatItemContextProvider = ({
|
||||
showRouteToAppDetail,
|
||||
showRouteToDatasetDetail,
|
||||
isShowReadRawSource,
|
||||
showNodeStatus
|
||||
// isShowFullText,
|
||||
showNodeStatus,
|
||||
quoteData,
|
||||
setQuoteData
|
||||
]);
|
||||
|
||||
return <ChatItemContext.Provider value={contextValue}>{children}</ChatItemContext.Provider>;
|
||||
|
||||
@@ -65,6 +65,7 @@ import type {
|
||||
listExistIdResponse
|
||||
} from '@/pages/api/core/dataset/apiDataset/listExistId';
|
||||
import { GetQuoteDataResponse } from '@/pages/api/core/dataset/data/getQuoteData';
|
||||
import { GetQuotePermissionResponse } from '@/pages/api/core/dataset/data/getPermission';
|
||||
|
||||
/* ======================== dataset ======================= */
|
||||
export const getDatasets = (data: GetDatasetListBody) =>
|
||||
@@ -179,6 +180,9 @@ export const getAllTags = (datasetId: string) =>
|
||||
export const getDatasetDataList = (data: GetDatasetDataListProps) =>
|
||||
POST<GetDatasetDataListRes>(`/core/dataset/data/v2/list`, data);
|
||||
|
||||
export const getDatasetDataPermission = (id?: string) =>
|
||||
GET<GetQuotePermissionResponse>(`/core/dataset/data/getPermission`, { id });
|
||||
|
||||
export const getDatasetDataItemById = (id: string) =>
|
||||
GET<DatasetDataItemType>(`/core/dataset/data/detail`, { id });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user