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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user