fix chat quote reader (#4125)

This commit is contained in:
heheer 2025-03-12 18:09:41 +08:00 committed by archer
parent 19f0f110e2
commit 8bc29e6527
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
14 changed files with 167 additions and 125 deletions

View File

@ -211,7 +211,7 @@ export function useLinkedScroll<
// 初始加载 // 初始加载
useEffect(() => { useEffect(() => {
if (canLoadData) { if (canLoadData) {
// 重置初始滚动状态 setInitialLoadDone(false);
hasScrolledToInitial.current = false; hasScrolledToInitial.current = false;
loadData({ loadData({
@ -225,10 +225,10 @@ export function useLinkedScroll<
// 监听初始加载完成,执行初始滚动 // 监听初始加载完成,执行初始滚动
useEffect(() => { useEffect(() => {
if (initialLoadDone && dataList.length > 0 && !hasScrolledToInitial.current) { if (initialLoadDone && dataList.length > 0 && !hasScrolledToInitial.current) {
hasScrolledToInitial.current = true;
const foundIndex = dataList.findIndex((item) => item._id === initialId); const foundIndex = dataList.findIndex((item) => item._id === initialId);
if (foundIndex >= 0) { if (foundIndex >= 0) {
hasScrolledToInitial.current = true;
setTimeout(() => { setTimeout(() => {
scrollToItem(foundIndex); scrollToItem(foundIndex);
}, 200); }, 200);
@ -299,7 +299,6 @@ export function useLinkedScroll<
setDataList, setDataList,
isLoading, isLoading,
loadData, loadData,
initialLoadDone,
ScrollData, ScrollData,
itemRefs, itemRefs,
scrollToItem scrollToItem

View File

@ -457,6 +457,7 @@
"core.chat.quote.Read Quote": "View Quote", "core.chat.quote.Read Quote": "View Quote",
"core.chat.quote.afterUpdate": "After update", "core.chat.quote.afterUpdate": "After update",
"core.chat.quote.beforeUpdate": "Before update", "core.chat.quote.beforeUpdate": "Before update",
"core.chat.quote.source": "From: {{source}}",
"core.chat.response.Complete Response": "Complete Response", "core.chat.response.Complete Response": "Complete Response",
"core.chat.response.Extension model": "Question Optimization Model", "core.chat.response.Extension model": "Question Optimization Model",
"core.chat.response.Read complete response": "View Details", "core.chat.response.Read complete response": "View Details",

View File

@ -460,6 +460,7 @@
"core.chat.quote.Read Quote": "查看引用", "core.chat.quote.Read Quote": "查看引用",
"core.chat.quote.afterUpdate": "更新后", "core.chat.quote.afterUpdate": "更新后",
"core.chat.quote.beforeUpdate": "更新前", "core.chat.quote.beforeUpdate": "更新前",
"core.chat.quote.source": "来源:{{source}}",
"core.chat.response.Complete Response": "完整响应", "core.chat.response.Complete Response": "完整响应",
"core.chat.response.Extension model": "问题优化模型", "core.chat.response.Extension model": "问题优化模型",
"core.chat.response.Read complete response": "查看详情", "core.chat.response.Read complete response": "查看详情",

View File

@ -456,6 +456,7 @@
"core.chat.quote.Read Quote": "檢視引用", "core.chat.quote.Read Quote": "檢視引用",
"core.chat.quote.afterUpdate": "更新後", "core.chat.quote.afterUpdate": "更新後",
"core.chat.quote.beforeUpdate": "更新前", "core.chat.quote.beforeUpdate": "更新前",
"core.chat.quote.source": "來源:{{source}}",
"core.chat.response.Complete Response": "完整回應", "core.chat.response.Complete Response": "完整回應",
"core.chat.response.Extension model": "問題最佳化模型", "core.chat.response.Extension model": "問題最佳化模型",
"core.chat.response.Read complete response": "檢視詳細資料", "core.chat.response.Read complete response": "檢視詳細資料",

View File

@ -1,35 +1,26 @@
import React, { useState, useEffect, useRef } from 'react'; import React from 'react';
import { Box, Flex } from '@chakra-ui/react'; import { Box, Flex } from '@chakra-ui/react';
import type { BoxProps } from '@chakra-ui/react'; import type { BoxProps } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
interface Props extends BoxProps { interface Props extends BoxProps {
externalTrigger?: Boolean; isFolded?: boolean;
onFoldChange?: (isFolded: boolean) => void;
} }
const SideBar = (e?: Props) => { const SideBar = (e?: Props) => {
const { const {
w = ['100%', '0 0 250px', '0 0 250px', '0 0 270px', '0 0 290px'], w = ['100%', '0 0 250px', '0 0 250px', '0 0 270px', '0 0 290px'],
children, children,
externalTrigger, isFolded = false,
onFoldChange,
...props ...props
} = e || {}; } = e || {};
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 handleToggle = () => {
const newFolded = !isFolded; if (onFoldChange) {
setIsFolded(newFolded); onFoldChange(!isFolded);
}
}; };
return ( return (

View File

@ -77,20 +77,18 @@ const CollectionQuoteItem = ({
}} }}
> >
{updated && ( {updated && (
<Flex <Flex mt={2}>
position={'absolute'} <Box
top={2} bg={'green.50'}
right={5} border={'1px solid'}
gap={1} borderRadius={'xs'}
bg={'yellow.50'} borderColor={'green.100'}
color={'yellow.500'} px={1}
px={2} color={'green.600'}
py={1} >
rounded={'md'} {t('common:core.dataset.data.Updated')}
fontSize={'12px'} </Box>
> <Box flex={1} borderBottom={'1px dashed'} borderColor={'green.200'} />
<MyIcon name="common/info" w={'14px'} color={'yellow.500'} />
{t('common:core.dataset.data.Updated')}
</Flex> </Flex>
)} )}
<Markdown source={q} /> <Markdown source={q} />

View File

@ -18,7 +18,6 @@ import { GetCollectionQuoteDataProps } from '@/web/core/chat/context/chatItemCon
import { useUserStore } from '@/web/support/user/useUserStore'; import { useUserStore } from '@/web/support/user/useUserStore';
import { getCollectionQuote } from '@/web/core/chat/api'; import { getCollectionQuote } from '@/web/core/chat/api';
import MyIconButton from '@fastgpt/web/components/common/Icon/button'; import MyIconButton from '@fastgpt/web/components/common/Icon/button';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyBox from '@fastgpt/web/components/common/MyBox'; import MyBox from '@fastgpt/web/components/common/MyBox';
import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource'; import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollectionSource';
import { QuoteDataItemType } from '@/service/core/chat/constants'; import { QuoteDataItemType } from '@/service/core/chat/constants';
@ -40,10 +39,10 @@ const CollectionReader = ({
const [quoteIndex, setQuoteIndex] = useState(0); const [quoteIndex, setQuoteIndex] = useState(0);
// Get dataset permission // Get dataset permission
const { data: permissionData, loading: isPermissionLoading } = useRequest2( const { data: datasetData, loading: isPermissionLoading } = useRequest2(
async () => await getDatasetDataPermission(datasetId), async () => await getDatasetDataPermission(datasetId),
{ {
manual: !userInfo && !datasetId, manual: !userInfo || !datasetId,
refreshDeps: [datasetId, userInfo] refreshDeps: [datasetId, userInfo]
} }
); );
@ -167,21 +166,7 @@ const CollectionReader = ({
> >
{sourceName || t('common:common.UnKnow Source')} {sourceName || t('common:common.UnKnow Source')}
</Box> </Box>
{!!userInfo && permissionData?.permission?.hasReadPer && ( <Box ml={3}>
<MyTooltip label={t('chat:to_dataset')}>
<MyIconButton
ml={3}
icon="core/dataset/datasetLight"
size="1rem"
onClick={() => {
router.push(
`/dataset/detail?datasetId=${datasetId}&currentTab=dataCard&collectionId=${collectionId}`
);
}}
/>
</MyTooltip>
)}
<Box ml={1}>
<DownloadButton <DownloadButton
canAccessRawData={true} canAccessRawData={true}
onDownload={handleDownload} onDownload={handleDownload}
@ -196,69 +181,94 @@ const CollectionReader = ({
onClick={onClose} onClick={onClose}
/> />
</HStack> </HStack>
<Box fontSize={'mini'} color={'myGray.500'}> {!isPermissionLoading && (
{t('common:core.chat.quote.Quote Tip')} <Box
</Box> fontSize={'mini'}
color={'myGray.500'}
onClick={() => {
if (!!userInfo && datasetData?.permission?.hasReadPer) {
router.push(
`/dataset/detail?datasetId=${datasetId}&currentTab=dataCard&collectionId=${collectionId}`
);
}
}}
{...(!!userInfo && datasetData?.permission?.hasReadPer
? {
cursor: 'pointer',
_hover: { color: 'primary.600', textDecoration: 'underline' }
}
: {})}
>
{t('common:core.chat.quote.source', {
source: datasetData?.datasetName
})}
</Box>
)}
</Box> </Box>
{/* header control */} {/* header control */}
{datasetDataList.length > 0 && ( {datasetDataList.length > 0 && (
<Flex <Box>
w={'full'} <Flex
px={4} w={'full'}
py={2} px={4}
alignItems={'center'} py={2}
borderBottom={'1px solid'} alignItems={'center'}
borderColor={'myGray.150'} borderBottom={'1px solid'}
> borderColor={'myGray.150'}
{/* 引用序号 */} >
<Flex fontSize={'mini'} mr={3} alignItems={'center'} gap={1}> {/* 引用序号 */}
<Box as={'span'} color={'myGray.900'}> <Flex fontSize={'mini'} mr={3} alignItems={'center'} gap={1}>
{t('common:core.chat.Quote')} {quoteIndex + 1} <Box as={'span'} color={'myGray.900'}>
</Box> {t('common:core.chat.Quote')} {quoteIndex + 1}
<Box as={'span'} color={'myGray.500'}> </Box>
/ <Box as={'span'} color={'myGray.500'}>
</Box> /
<Box as={'span'} color={'myGray.500'}> </Box>
{filterResults.length} <Box as={'span'} color={'myGray.500'}>
</Box> {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> </Flex>
<Box fontSize={'mini'} color={'myGray.500'} bg={'myGray.25'} px={4} py={1}>
{/* 检索分数 */} {t('common:core.chat.quote.Quote Tip')}
{!loading && </Box>
(!isDeleted ? ( </Box>
<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 */} {/* quote list */}
@ -282,7 +292,7 @@ const CollectionReader = ({
a={item.a} a={item.a}
dataId={item._id} dataId={item._id}
collectionId={collectionId} collectionId={collectionId}
canEdit={!!userInfo && !!permissionData?.permission?.hasWritePer} canEdit={!!userInfo && !!datasetData?.permission?.hasWritePer}
/> />
))} ))}
</Flex> </Flex>

View File

@ -17,7 +17,17 @@ const DownloadButton = ({
return ( return (
<MyMenu <MyMenu
size={'xs'} size={'xs'}
Button={<MyIconButton icon="common/download" size={'1rem'} />} Button={
<MyIconButton
icon="common/download"
size={'1rem'}
border={'1px solid'}
borderColor={'myGray.250'}
boxShadow={
'0px 1px 2px 0px rgba(19, 51, 107, 0.05), 0px 0px 1px 0px rgba(19, 51, 107, 0.08)'
}
/>
}
menuList={[ menuList={[
{ {
children: [ children: [

View File

@ -1,9 +1,8 @@
import React from 'react'; import React from 'react';
import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import { ChatItemContext, GetQuoteProps } from '@/web/core/chat/context/chatItemContext'; import { GetQuoteProps } from '@/web/core/chat/context/chatItemContext';
import CollectionQuoteReader from './CollectionQuoteReader'; import CollectionQuoteReader from './CollectionQuoteReader';
import QuoteReader from './QuoteReader'; import QuoteReader from './QuoteReader';
import { useContextSelector } from 'use-context-selector';
const ChatQuoteList = ({ const ChatQuoteList = ({
rawSearch = [], rawSearch = [],
@ -14,8 +13,6 @@ const ChatQuoteList = ({
metadata: GetQuoteProps; metadata: GetQuoteProps;
onClose: () => void; onClose: () => void;
}) => { }) => {
const isShowReadRawSource = useContextSelector(ChatItemContext, (v) => v.isShowReadRawSource);
return ( return (
<> <>
{'collectionId' in metadata && ( {'collectionId' in metadata && (

View File

@ -6,6 +6,7 @@ import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
export type GetQuotePermissionResponse = export type GetQuotePermissionResponse =
| { | {
datasetName: string;
permission: { permission: {
hasWritePer: boolean; hasWritePer: boolean;
hasReadPer: boolean; hasReadPer: boolean;
@ -22,7 +23,7 @@ async function handler(req: NextApiRequest): Promise<GetQuotePermissionResponse>
} }
try { try {
const { permission } = await authDataset({ const { permission, dataset } = await authDataset({
req, req,
authToken: true, authToken: true,
authApiKey: true, authApiKey: true,
@ -31,6 +32,7 @@ async function handler(req: NextApiRequest): Promise<GetQuotePermissionResponse>
}); });
return { return {
datasetName: dataset.name,
permission: { permission: {
hasReadPer: permission.hasReadPer, hasReadPer: permission.hasReadPer,
hasWritePer: permission.hasWritePer hasWritePer: permission.hasWritePer
@ -39,6 +41,7 @@ async function handler(req: NextApiRequest): Promise<GetQuotePermissionResponse>
} catch (error) { } catch (error) {
if (error === DatasetErrEnum.unAuthDataset) { if (error === DatasetErrEnum.unAuthDataset) {
return { return {
datasetName: '',
permission: { permission: {
hasWritePer: false, hasWritePer: false,
hasReadPer: false hasReadPer: false

View File

@ -65,6 +65,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const [sidebarFolded, setSidebarFolded] = useState(false);
useEffect(() => {
if (quoteData) {
setSidebarFolded(true);
}
}, [quoteData]);
// Load chat init data // Load chat init data
const { loading } = useRequest2( const { loading } = useRequest2(
async () => { async () => {
@ -148,7 +156,9 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
); );
return isPc || !appId ? ( return isPc || !appId ? (
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar> <SideBar isFolded={sidebarFolded} onFoldChange={setSidebarFolded}>
{Children}
</SideBar>
) : ( ) : (
<Drawer <Drawer
isOpen={isOpenSlider} isOpen={isOpenSlider}
@ -161,7 +171,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent> <DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
</Drawer> </Drawer>
); );
}, [t, isPc, appId, isOpenSlider, onCloseSlider, quoteData]); }, [t, isPc, appId, isOpenSlider, onCloseSlider, sidebarFolded]);
return ( return (
<Flex h={'100%'}> <Flex h={'100%'}>

View File

@ -90,6 +90,14 @@ const OutLink = (props: Props) => {
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded); const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const [sidebarFolded, setSidebarFolded] = useState(false);
useEffect(() => {
if (quoteData) {
setSidebarFolded(true);
}
}, [quoteData]);
const initSign = useRef(false); const initSign = useRef(false);
const { data, loading } = useRequest2( const { data, loading } = useRequest2(
async () => { async () => {
@ -221,7 +229,9 @@ const OutLink = (props: Props) => {
if (showHistory !== '1') return null; if (showHistory !== '1') return null;
return isPc ? ( return isPc ? (
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar> <SideBar isFolded={sidebarFolded} onFoldChange={setSidebarFolded}>
{Children}
</SideBar>
) : ( ) : (
<Drawer <Drawer
isOpen={isOpenSlider} isOpen={isOpenSlider}
@ -236,7 +246,7 @@ const OutLink = (props: Props) => {
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
); );
}, [isOpenSlider, isPc, onCloseSlider, quoteData, showHistory, t]); }, [isOpenSlider, isPc, onCloseSlider, showHistory, t, sidebarFolded]);
return ( return (
<> <>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import NextHead from '@/components/common/NextHead'; import NextHead from '@/components/common/NextHead';
import { getTeamChatInfo } from '@/web/core/chat/api'; import { getTeamChatInfo } from '@/web/core/chat/api';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@ -70,6 +70,14 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords); const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount); const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const [sidebarFolded, setSidebarFolded] = useState(false);
useEffect(() => {
if (quoteData) {
setSidebarFolded(true);
}
}, [quoteData]);
// get chat app info // get chat app info
const { loading } = useRequest2( const { loading } = useRequest2(
async () => { async () => {
@ -166,7 +174,9 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
); );
return isPc || !appId ? ( return isPc || !appId ? (
<SideBar externalTrigger={!!quoteData}>{Children}</SideBar> <SideBar isFolded={sidebarFolded} onFoldChange={setSidebarFolded}>
{Children}
</SideBar>
) : ( ) : (
<Drawer <Drawer
isOpen={isOpenSlider} isOpen={isOpenSlider}
@ -179,7 +189,7 @@ const Chat = ({ myApps }: { myApps: AppListItemType[] }) => {
<DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent> <DrawerContent maxWidth={'75vw'}>{Children}</DrawerContent>
</Drawer> </Drawer>
); );
}, [appId, isOpenSlider, isPc, onCloseSlider, quoteData, t]); }, [appId, isOpenSlider, isPc, onCloseSlider, t, sidebarFolded]);
return ( return (
<Flex h={'100%'}> <Flex h={'100%'}>

View File

@ -221,6 +221,7 @@ export async function updateData2Dataset({
} }
// 4. Update mongo updateTime(便于脏数据检查器识别) // 4. Update mongo updateTime(便于脏数据检查器识别)
const updateTime = mongoData.updateTime;
mongoData.updateTime = new Date(); mongoData.updateTime = new Date();
await mongoData.save(); await mongoData.save();
@ -258,7 +259,7 @@ export async function updateData2Dataset({
{ {
q: mongoData.q, q: mongoData.q,
a: mongoData.a, a: mongoData.a,
updateTime: new Date() updateTime: updateTime
}, },
...(mongoData.history?.slice(0, 9) || []) ...(mongoData.history?.slice(0, 9) || [])
] ]