chat quote reader (#3912)

* init chat quote full text reader

* linked structure

* dataset data linked

* optimize code

* fix ts build

* test finish

* delete log

* fix

* fix ts

* fix ts

* remove nextId

* initial scroll

* fix

* fix
This commit is contained in:
heheer
2025-03-11 19:44:33 +08:00
committed by archer
parent 16832caaf6
commit ac7091f8d6
64 changed files with 2676 additions and 369 deletions

View File

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

View File

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

View File

@@ -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>
))}
</>
);
});

View File

@@ -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} />
)}
</>
);
};

View File

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

View File

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

View File

@@ -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')} />
)}

View File

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

View File

@@ -39,5 +39,6 @@ export type DatasetDataListItemType = {
q: string; // embedding content
a: string; // bonus content
chunkIndex?: number;
updated?: boolean;
// indexes: DatasetDataSchemaType['indexes'];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,192 @@
import Markdown from '@/components/Markdown';
import { Box, Flex } from '@chakra-ui/react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Dispatch, MutableRefObject, SetStateAction, useState } from 'react';
import { useTranslation } from 'react-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
import InputDataModal from '@/pageComponents/dataset/detail/InputDataModal';
const CollectionQuoteItem = ({
index,
quoteRefs,
quoteIndex,
setQuoteIndex,
refreshList,
canEdit,
updated,
isCurrentSelected,
q,
a,
dataId,
collectionId
}: {
index: number;
quoteRefs: MutableRefObject<(HTMLDivElement | null)[]>;
quoteIndex: number;
setQuoteIndex: Dispatch<SetStateAction<number>>;
refreshList: () => void;
canEdit: boolean;
updated?: boolean;
isCurrentSelected: boolean;
q: string;
a?: string;
dataId: string;
collectionId: string;
}) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const hasBeenSearched = quoteIndex !== undefined && quoteIndex > -1;
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
return (
<>
<Box
ref={(el: HTMLDivElement | null) => {
quoteRefs.current[index] = el;
}}
p={2}
py={2}
cursor={hasBeenSearched ? 'pointer' : 'default'}
bg={isCurrentSelected ? '#FFF9E7' : hasBeenSearched ? '#FFFCF2' : ''}
position={'relative'}
overflow={'hidden'}
border={'1px solid '}
borderColor={isCurrentSelected ? 'yellow.200' : 'transparent'}
wordBreak={'break-all'}
fontSize={'sm'}
_hover={
hasBeenSearched
? {
'& .hover-data': { visibility: 'visible' }
}
: {
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
borderTopColor: 'myGray.50',
'& .hover-data': { visibility: 'visible' }
}
}
onClick={(e) => {
e.stopPropagation();
if (hasBeenSearched) {
setQuoteIndex(quoteIndex);
}
}}
>
{updated && (
<Flex
position={'absolute'}
top={2}
right={5}
gap={1}
bg={'yellow.50'}
color={'yellow.500'}
px={2}
py={1}
rounded={'md'}
fontSize={'12px'}
>
<MyIcon name="common/info" w={'14px'} color={'yellow.500'} />
{t('common:core.dataset.data.Updated')}
</Flex>
)}
<Markdown source={q} />
{!!a && (
<Box>
<Markdown source={a} />
</Box>
)}
<Flex
className="hover-data"
position={'absolute'}
bottom={2}
right={5}
gap={1.5}
visibility={'hidden'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={2}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{q.length + (a?.length || 0)}
</Flex>
</MyTooltip>
{canEdit && (
<MyTooltip label={t('common:core.dataset.data.Edit')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={1}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
cursor={'pointer'}
onClick={() =>
setEditInputData({
dataId,
collectionId
})
}
>
<MyIcon name="common/edit" w={'14px'} color={'myGray.500'} />
</Flex>
</MyTooltip>
)}
<MyTooltip label={t('common:common.Copy')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={1}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
cursor={'pointer'}
onClick={() => {
copyData(q + '\n' + a);
}}
>
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
</Flex>
</MyTooltip>
</Flex>
</Box>
{editInputData && (
<InputDataModal
onClose={() => setEditInputData(undefined)}
onSuccess={() => {
console.log('onSuccess');
refreshList();
}}
dataId={editInputData.dataId}
collectionId={editInputData.collectionId}
/>
)}
</>
);
};
export default CollectionQuoteItem;

View File

@@ -0,0 +1,345 @@
import { Box, Button, Flex } from '@chakra-ui/react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
import DownloadButton from './DownloadButton';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { downloadFetch } from '@/web/common/system/utils';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { getCollectionSource, getDatasetDataPermission } from '@/web/core/dataset/api';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import ScoreTag from './ScoreTag';
import { formatScore } from '@/components/core/dataset/QuoteItem';
import NavButton from './NavButton';
import { useLinkedScroll } from '@fastgpt/web/hooks/useLinkedScroll';
import CollectionQuoteItem from './CollectionQuoteItem';
import { DatasetDataListItemType } from '@/global/core/dataset/type';
import { metadataType } from '@/web/core/chat/context/chatItemContext';
import { useUserStore } from '@/web/support/user/useUserStore';
import { getCollectionQuote } from '@/web/core/chat/api';
const CollectionReader = ({
rawSearch,
metadata,
chatTime,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
metadata: metadataType;
chatTime: Date;
onClose: () => void;
}) => {
const { t } = useTranslation();
const router = useRouter();
const { toast } = useToast();
const { chatId, appId, outLinkAuthData } = useChatStore();
const { userInfo } = useUserStore();
const { collectionId, datasetId, chatItemId, sourceId, sourceName } = metadata;
const [quoteIndex, setQuoteIndex] = useState(0);
const { data: permissionData, loading: isPermissionLoading } = useRequest2(
async () => await getDatasetDataPermission(datasetId),
{
manual: !userInfo && !datasetId,
refreshDeps: [datasetId, userInfo]
}
);
const filterResults = useMemo(() => {
const results = rawSearch.filter(
(item) => item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
);
return results.sort((a, b) => (a.chunkIndex || 0) - (b.chunkIndex || 0));
}, [metadata, rawSearch]);
const currentQuoteItem = filterResults[quoteIndex];
const {
dataList: datasetDataList,
setDataList: setDatasetDataList,
isLoading,
loadData,
ScrollData,
itemRefs,
scrollToItem
} = useLinkedScroll(getCollectionQuote, {
refreshDeps: [collectionId],
params: {
collectionId,
chatTime,
chatItemId,
chatId,
appId,
...outLinkAuthData
},
initialId: currentQuoteItem?.id,
initialIndex: currentQuoteItem?.chunkIndex,
canLoadData: !!currentQuoteItem?.id && !isPermissionLoading
});
const loading = isLoading || isPermissionLoading;
const isDeleted = !datasetDataList.find((item) => item._id === currentQuoteItem?.id);
const formatedDataList = useMemo(
() =>
datasetDataList.map((item: DatasetDataListItemType) => {
const isCurrentSelected = currentQuoteItem?.id === item._id;
const quoteIndex = filterResults.findIndex((res) => res.id === item._id);
return {
...item,
isCurrentSelected,
quoteIndex
};
}),
[currentQuoteItem?.id, datasetDataList, filterResults]
);
useEffect(() => {
setQuoteIndex(0);
setDatasetDataList([]);
}, [collectionId, setDatasetDataList]);
const { runAsync: handleDownload, loading: downloadLoading } = useRequest2(async () => {
await downloadFetch({
url: '/api/core/dataset/collection/export',
filename: 'parsed_content.md',
body: {
collectionId: collectionId,
chatTime: chatTime,
chatItemId: chatItemId
}
});
});
const { runAsync: handleRead, loading: readLoading } = useRequest2(
async () => await getCollectionSource({ ...metadata, appId, chatId }),
{
onSuccess: (res) => {
if (!res.value) {
throw new Error('No file found');
}
if (res.value.startsWith('/')) {
window.open(`${location.origin}${res.value}`, '_blank');
} else {
window.open(res.value, '_blank');
}
},
onError: (err) => {
toast({
title: t(getErrText(err, t('common:error.fileNotFound'))),
status: 'error'
});
}
}
);
const handleNavigate = useCallback(
async (targetIndex: number) => {
if (targetIndex < 0 || targetIndex >= filterResults.length) return;
const targetItemId = filterResults[targetIndex].id;
const targetItemIndex = filterResults[targetIndex].chunkIndex;
setQuoteIndex(targetIndex);
const dataIndex = datasetDataList.findIndex((item) => item._id === targetItemId);
if (dataIndex !== -1) {
setTimeout(() => {
scrollToItem(dataIndex);
}, 50);
} else {
try {
await loadData({ id: targetItemId, index: targetItemIndex });
} catch (error) {
console.error('Failed to navigate:', error);
}
}
},
[filterResults, datasetDataList, scrollToItem, loadData]
);
return (
<Flex flexDirection={'column'} h={'full'}>
{/* title */}
<Flex
w={'full'}
alignItems={'center'}
px={5}
borderBottom={'1px solid'}
borderColor={'myGray.150'}
>
<Box flex={1} py={4}>
<Flex mb={1} alignItems={['flex-start', 'center']} flexDirection={['column', 'row']}>
<Flex gap={2} mr={2}>
<MyIcon
name={getSourceNameIcon({ sourceId, sourceName }) as any}
w={['1rem', '1.25rem']}
color={'primary.600'}
/>
<Box
maxW={['200px', '300px']}
className={'textEllipsis'}
wordBreak={'break-all'}
color={'myGray.900'}
fontWeight={'medium'}
>
{sourceName || t('common:common.UnKnow Source')}
</Box>
</Flex>
<Flex gap={3} mt={[2, 0]} alignItems={'center'}>
{!!userInfo && permissionData?.permission?.hasReadPer && (
<Button
variant={'primaryGhost'}
size={'xs'}
fontSize={'mini'}
border={'none'}
_hover={{
bg: 'primary.100'
}}
onClick={() => {
router.push(
`/dataset/detail?datasetId=${datasetId}&currentTab=dataCard&collectionId=${collectionId}`
);
}}
>
{t('common:core.dataset.Go Dataset')}
<MyIcon name="common/upperRight" w={4} ml={1} />
</Button>
)}
<DownloadButton
canAccessRawData={true}
onDownload={handleDownload}
onRead={handleRead}
isLoading={downloadLoading || readLoading}
/>
</Flex>
</Flex>
<Box fontSize={'mini'} color={'myGray.500'}>
{t('common:core.chat.quote.Quote Tip')}
</Box>
</Box>
<Box
cursor={'pointer'}
borderRadius={'sm'}
p={1}
_hover={{
bg: 'myGray.100'
}}
onClick={onClose}
>
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
</Box>
</Flex>
{/* header control */}
{datasetDataList.length > 0 && (
<Flex
w={'full'}
px={4}
py={2}
alignItems={'center'}
borderBottom={'1px solid'}
borderColor={'myGray.150'}
>
{/* 引用序号 */}
<Flex fontSize={'mini'} mr={3} alignItems={'center'} gap={1}>
<Box as={'span'} color={'myGray.900'}>
{t('common:core.chat.Quote')} {quoteIndex + 1}
</Box>
<Box as={'span'} color={'myGray.500'}>
/
</Box>
<Box as={'span'} color={'myGray.500'}>
{filterResults.length}
</Box>
</Flex>
{/* 检索分数 */}
{!loading &&
(!isDeleted ? (
<ScoreTag {...formatScore(currentQuoteItem?.score)} />
) : (
<Flex
borderRadius={'sm'}
py={1}
px={2}
color={'red.600'}
bg={'red.50'}
alignItems={'center'}
fontSize={'11px'}
>
<MyIcon name="common/info" w={'14px'} mr={1} color={'red.600'} />
{t('chat:chat.quote.deleted')}
</Flex>
))}
<Box flex={1} />
{/* 检索按钮 */}
<Flex gap={1}>
<NavButton
direction="up"
isDisabled={quoteIndex === 0}
onClick={() => handleNavigate(quoteIndex - 1)}
/>
<NavButton
direction="down"
isDisabled={quoteIndex === filterResults.length - 1}
onClick={() => handleNavigate(quoteIndex + 1)}
/>
</Flex>
</Flex>
)}
{/* quote list */}
{loading || datasetDataList.length > 0 ? (
<ScrollData flex={'1 0 0'} mt={2} px={5} py={1} isLoading={loading}>
<Flex flexDir={'column'} gap={3}>
{formatedDataList.map((item, index) => (
<CollectionQuoteItem
key={item._id}
index={index}
quoteRefs={itemRefs as React.MutableRefObject<(HTMLDivElement | null)[]>}
quoteIndex={item.quoteIndex}
setQuoteIndex={setQuoteIndex}
refreshList={() =>
currentQuoteItem?.id &&
loadData({ id: currentQuoteItem.id, index: currentQuoteItem.chunkIndex })
}
updated={item.updated}
isCurrentSelected={item.isCurrentSelected}
q={item.q}
a={item.a}
dataId={item._id}
collectionId={collectionId}
canEdit={!!userInfo && !!permissionData?.permission?.hasWritePer}
/>
))}
</Flex>
</ScrollData>
) : (
<Flex
flex={'1 0 0'}
flexDirection={'column'}
gap={1}
justifyContent={'center'}
alignItems={'center'}
>
<Box border={'1px dashed'} borderColor={'myGray.400'} p={2} borderRadius={'full'}>
<MyIcon name="common/fileNotFound" />
</Box>
<Box fontSize={'sm'} color={'myGray.500'}>
{t('chat:chat.quote.No Data')}
</Box>
</Flex>
)}
</Flex>
);
};
export default CollectionReader;

View File

@@ -0,0 +1,68 @@
import { Button } from '@chakra-ui/react';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useTranslation } from 'react-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
const DownloadButton = ({
canAccessRawData,
onDownload,
onRead,
isLoading
}: {
canAccessRawData: boolean;
onDownload: () => void;
onRead: () => void;
isLoading: boolean;
}) => {
const { t } = useTranslation();
if (canAccessRawData) {
return (
<MyMenu
size={'xs'}
Button={
<Button
variant={'whitePrimary'}
size={'xs'}
fontSize={'mini'}
leftIcon={<MyIcon name={'common/download'} w={'4'} />}
isLoading={isLoading}
>
{t('common:Download')}
</Button>
}
menuList={[
{
children: [
{
label: t('common:core.dataset.Download the parsed content'),
type: 'grayBg',
onClick: onDownload
},
{
label: t('common:core.dataset.Get the raw data'),
type: 'grayBg',
onClick: onRead
}
]
}
]}
/>
);
}
return (
<Button
variant={'whitePrimary'}
size={'xs'}
fontSize={'mini'}
leftIcon={<MyIcon name={'common/download'} w={'4'} />}
onClick={onDownload}
isLoading={isLoading}
>
{t('common:Download')}
</Button>
);
};
export default DownloadButton;

View File

@@ -0,0 +1,47 @@
import { Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
const NavButton = ({
direction,
isDisabled,
onClick
}: {
direction: 'up' | 'down';
isDisabled: boolean;
onClick: () => void;
}) => {
const isUp = direction === 'up';
const baseStyles = {
color: 'myGray.500',
border: '1px solid',
borderColor: 'myGray.150',
borderRadius: 'sm',
w: 6,
h: 6,
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s'
};
const stateStyles = isDisabled
? {
cursor: 'not-allowed',
opacity: 0.5,
_hover: {}
}
: {
cursor: 'pointer',
opacity: 1,
_hover: { bg: 'myGray.100' },
onClick
};
return (
<Flex {...baseStyles} {...stateStyles}>
<MyIcon name={isUp ? `common/solidChevronUp` : `common/solidChevronDown`} w={'18px'} />
</Flex>
);
};
export default NavButton;

View File

@@ -0,0 +1,163 @@
import { ScoreItemType } from '@/components/core/dataset/QuoteItem';
import { Box, Flex } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import ScoreTag from './ScoreTag';
import Markdown from '@/components/Markdown';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'react-i18next';
import { useCopyData } from '@fastgpt/web/hooks/useCopyData';
const QuoteItem = ({
index,
icon,
sourceName,
score,
q,
a
}: {
index: number;
icon: string;
sourceName: string;
score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] };
q: string;
a?: string;
}) => {
const { t } = useTranslation();
const { copyData } = useCopyData();
const isDeleted = !q;
return (
<Box
p={2}
position={'relative'}
overflow={'hidden'}
border={'1px solid transparent'}
borderBottomColor={'myGray.150'}
wordBreak={'break-all'}
fontSize={'sm'}
_hover={{
bg: 'linear-gradient(180deg, #FBFBFC 7.61%, #F0F1F6 100%)',
borderTopColor: 'myGray.50',
'& .hover-data': { visibility: 'visible' }
}}
>
<Flex gap={2} alignItems={'center'} mb={2}>
<Box
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
borderRadius={'sm'}
_hover={{
'.controller': {
display: 'flex'
}
}}
overflow={'hidden'}
display={'inline-flex'}
height={6}
>
<Flex
color={'myGray.500'}
bg={'myGray.150'}
w={4}
justifyContent={'center'}
fontSize={'10px'}
h={'full'}
alignItems={'center'}
>
{index + 1}
</Flex>
<Flex px={1.5}>
<MyIcon name={icon as any} mr={1} flexShrink={0} w={'12px'} />
<Box
className="textEllipsis3"
wordBreak={'break-all'}
flex={'1 0 0'}
fontSize={'mini'}
color={'myGray.900'}
>
{sourceName}
</Box>
</Flex>
</Box>
{score && !isDeleted && (
<Box className="hover-data" visibility={'hidden'}>
<ScoreTag {...score} />
</Box>
)}
</Flex>
{!isDeleted ? (
<>
<Markdown source={q} />
{!!a && (
<Box>
<Markdown source={a} />
</Box>
)}
</>
) : (
<Flex
justifyContent={'center'}
alignItems={'center'}
h={'full'}
py={2}
bg={'#FAFAFA'}
color={'myGray.500'}
>
<MyIcon name="common/info" w={'14px'} mr={1} color={'myGray.500'} />
{t('chat:chat.quote.deleted')}
</Flex>
)}
<Flex
className="hover-data"
position={'absolute'}
bottom={2}
right={5}
gap={1.5}
visibility={'hidden'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={2}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{q.length + (a?.length || 0)}
</Flex>
</MyTooltip>
<MyTooltip label={t('common:common.Copy')}>
<Flex
alignItems={'center'}
fontSize={'10px'}
border={'1px solid'}
borderColor={'myGray.200'}
bg={'white'}
rounded={'sm'}
px={1}
py={1}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
cursor={'pointer'}
onClick={() => {
copyData(q + '\n' + a);
}}
>
<MyIcon name="copy" w={'14px'} color={'myGray.500'} />
</Flex>
</MyTooltip>
</Flex>
</Box>
);
};
export default QuoteItem;

View File

@@ -0,0 +1,152 @@
import { Box, Flex } from '@chakra-ui/react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { useTranslation } from 'react-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import QuoteItem from './QuoteItem';
import { useMemo } from 'react';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import { formatScore } from '@/components/core/dataset/QuoteItem';
import { metadataType } from '@/web/core/chat/context/chatItemContext';
import { getQuoteDataList } from '@/web/core/chat/api';
const QuoteReader = ({
rawSearch,
metadata,
chatTime,
onClose
}: {
rawSearch: SearchDataResponseItemType[];
metadata: metadataType;
chatTime: Date;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { chatId, appId, outLinkAuthData } = useChatStore();
const { data, loading } = useRequest2(
async () =>
await getQuoteDataList({
datasetDataIdList: rawSearch.map((item) => item.id),
chatTime,
collectionIdList: metadata.collectionIdList,
chatItemId: metadata.chatItemId,
appId,
chatId,
...outLinkAuthData
}),
{
manual: false
}
);
const filterResults = useMemo(() => {
if (!metadata.collectionId) {
return rawSearch;
}
return rawSearch.filter(
(item) => item.collectionId === metadata.collectionId && item.sourceId === metadata.sourceId
);
}, [metadata, rawSearch]);
const formatedDataList = useMemo(() => {
return filterResults
.map((item) => {
const currentFilterItem = data?.quoteList.find((res) => res._id === item.id);
return {
...item,
q: currentFilterItem?.q || '',
a: currentFilterItem?.a || '',
score: formatScore(item.score),
icon: getSourceNameIcon({
sourceId: item.sourceId,
sourceName: item.sourceName
})
};
})
.sort((a, b) => {
return (b.score.primaryScore?.value || 0) - (a.score.primaryScore?.value || 0);
});
}, [data?.quoteList, filterResults]);
return (
<Flex flexDirection={'column'} h={'full'}>
{/* title */}
<Flex
w={'full'}
alignItems={'center'}
px={5}
borderBottom={'1px solid'}
borderColor={'myGray.150'}
>
<Box flex={1} py={4}>
<Flex gap={2} mr={2} mb={1}>
<MyIcon
name={
metadata.sourceId && metadata.sourceName
? (getSourceNameIcon({
sourceId: metadata.sourceId,
sourceName: metadata.sourceName
}) as any)
: 'core/chat/quoteFill'
}
w={['1rem', '1.25rem']}
color={'primary.600'}
/>
<Box
maxW={['200px', '300px']}
className={'textEllipsis'}
wordBreak={'break-all'}
color={'myGray.900'}
fontWeight={'medium'}
>
{metadata.sourceName
? metadata.sourceName
: t('common:core.chat.Quote Amount', { amount: rawSearch.length })}
</Box>
</Flex>
<Box fontSize={'mini'} color={'myGray.500'}>
{t('common:core.chat.quote.Quote Tip')}
</Box>
</Box>
<Box
cursor={'pointer'}
borderRadius={'sm'}
p={1}
_hover={{
bg: 'myGray.100'
}}
onClick={onClose}
>
<MyIcon name="common/closeLight" color={'myGray.900'} w={6} />
</Box>
</Flex>
{/* quote list */}
<MyBox flex={'1 0 0'} mt={2} px={5} py={1} overflow={'auto'} isLoading={loading}>
{!loading && (
<Flex flexDir={'column'} gap={3}>
{formatedDataList?.map((item, index) => (
<QuoteItem
key={item.id}
index={index}
icon={item.icon}
sourceName={item.sourceName}
score={item.score}
q={item.q}
a={item.a}
/>
))}
</Flex>
)}
</MyBox>
</Flex>
);
};
export default QuoteReader;

View File

@@ -0,0 +1,78 @@
import { ScoreItemType, scoreTheme } from '@/components/core/dataset/QuoteItem';
import { Box, Flex, Progress } from '@chakra-ui/react';
import { SearchScoreTypeMap } from '@fastgpt/global/core/dataset/constants';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'react-i18next';
const ScoreTag = (score: { primaryScore?: ScoreItemType; secondaryScore: ScoreItemType[] }) => {
const { t } = useTranslation();
return (
<Flex alignItems={'center'} flexWrap={'wrap'} gap={3}>
{score?.primaryScore && (
<MyTooltip
label={
score.secondaryScore.length ? (
<Box>
{score.secondaryScore.map((item, i) => (
<Box fontSize={'xs'} key={i}>
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
<Box
px={'5px'}
borderWidth={'1px'}
borderRadius={'sm'}
mr={'2px'}
{...(scoreTheme[i] && scoreTheme[i])}
>
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
</Box>
<Box transform={'scale(0.9)'}>
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
</Box>
</Flex>
<Box h={'4px'}>
{SearchScoreTypeMap[item.type]?.showScore && (
<Progress
value={item.value * 100}
h={'4px'}
w={'100%'}
size="sm"
borderRadius={'20px'}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorScheme
})}
bg="#E8EBF0"
/>
)}
</Box>
</Box>
))}
</Box>
) : (
t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)
)
}
>
<Flex
borderRadius={'sm'}
py={1}
px={2}
color={'green.600'}
bg={'green.50'}
alignItems={'center'}
fontSize={'11px'}
>
<Box>
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
? ` ${score.primaryScore.value.toFixed(4)}`
: `: ${score.primaryScore.index + 1}`}
</Box>
</Flex>
</MyTooltip>
)}
</Flex>
);
};
export default ScoreTag;

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { useContextSelector } from 'use-context-selector';
import { ChatItemContext, metadataType } from '@/web/core/chat/context/chatItemContext';
import CollectionQuoteReader from './CollectionQuoteReader';
import QuoteReader from './QuoteReader';
const ChatQuoteList = ({
chatTime,
rawSearch = [],
metadata,
onClose
}: {
chatTime: Date;
rawSearch: SearchDataResponseItemType[];
metadata: metadataType;
onClose: () => void;
}) => {
const isShowReadRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
return (
<>
{metadata.collectionId && isShowReadRawSource ? (
<CollectionQuoteReader
rawSearch={rawSearch}
metadata={metadata}
chatTime={chatTime}
onClose={onClose}
/>
) : (
<QuoteReader
rawSearch={rawSearch}
metadata={metadata}
chatTime={chatTime}
onClose={onClose}
/>
)}
</>
);
};
export default ChatQuoteList;

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

@@ -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(),

View File

@@ -37,6 +37,7 @@ async function handler(
responseDetail,
showRawSource,
showNodeStatus,
// showFullText,
limit,
app
});

View File

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

View File

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

View File

@@ -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 ?? '',

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ export const defaultOutLinkForm: OutLinkEditType = {
name: '',
showNodeStatus: true,
responseDetail: false,
// showFullText: false,
showRawSource: false,
limit: {
QPM: 100,

View File

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

View File

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

View File

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