Commit 85de3c1d by papapatrick Committed by GitHub

Chat-perf (#2117)

* feat:  新增对话框底部复制按钮

* fix: 对话框底部复制按钮定位问题

* feat: 过长引用折叠功能

* merge: 合并主仓库代码

* refactor: 删除不必要代码

* doc: 文档补充的部分修改成英文
parent c6f68231
...@@ -23,12 +23,20 @@ pnpm dev ...@@ -23,12 +23,20 @@ pnpm dev
make dev name=app make dev name=app
``` ```
Note: If the Node version is >= 20, you need to pass the `--no-node-snapshot` parameter to Node when running `pnpm i`
```sh
NODE_OPTIONS=--no-node-snapshot pnpm i
```
## I18N ## I18N
### Install i18n-ally Plugin ### Install i18n-ally Plugin
1. Open the Extensions Marketplace in VSCode, search for and install the `i18n Ally` plugin. 1. Open the Extensions Marketplace in VSCode, search for and install the `i18n Ally` plugin.
### Code Optimization Examples ### Code Optimization Examples
#### Fetch Specific Namespace Translations in `getServerSideProps` #### Fetch Specific Namespace Translations in `getServerSideProps`
```typescript ```typescript
...@@ -42,7 +50,9 @@ export async function getServerSideProps(context: any) { ...@@ -42,7 +50,9 @@ export async function getServerSideProps(context: any) {
}; };
} }
``` ```
#### Use useTranslation Hook in Page #### Use useTranslation Hook in Page
```typescript ```typescript
// pages/yourPage.tsx // pages/yourPage.tsx
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
...@@ -64,7 +74,9 @@ const YourComponent = () => { ...@@ -64,7 +74,9 @@ const YourComponent = () => {
export default YourComponent; export default YourComponent;
``` ```
#### Handle Static File Translations #### Handle Static File Translations
```typescript ```typescript
// utils/i18n.ts // utils/i18n.ts
import { i18nT } from '@fastgpt/web/i18n/utils'; import { i18nT } from '@fastgpt/web/i18n/utils';
...@@ -77,12 +89,12 @@ const staticContent = { ...@@ -77,12 +89,12 @@ const staticContent = {
export default staticContent; export default staticContent;
``` ```
### Standardize Translation Format ### Standardize Translation Format
- Use the t(namespace:key) format to ensure consistent naming. - Use the t(namespace:key) format to ensure consistent naming.
- Translation keys should use lowercase letters and underscores, e.g., common.close. - Translation keys should use lowercase letters and underscores, e.g., common.close.
## Build ## Build
```sh ```sh
...@@ -96,4 +108,3 @@ docker build -f ./projects/app/Dockerfile -t registry.cn-hangzhou.aliyuncs.com/f ...@@ -96,4 +108,3 @@ docker build -f ./projects/app/Dockerfile -t registry.cn-hangzhou.aliyuncs.com/f
# Make cmd: Build image with proxy # Make cmd: Build image with proxy
make build name=app image=registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.1 proxy=taobao make build name=app image=registry.cn-hangzhou.aliyuncs.com/fastgpt/fastgpt:v4.8.1 proxy=taobao
``` ```
...@@ -106,6 +106,8 @@ export const iconPaths = { ...@@ -106,6 +106,8 @@ export const iconPaths = {
'core/chat/fileSelect': () => import('./icons/core/chat/fileSelect.svg'), 'core/chat/fileSelect': () => import('./icons/core/chat/fileSelect.svg'),
'core/chat/finishSpeak': () => import('./icons/core/chat/finishSpeak.svg'), 'core/chat/finishSpeak': () => import('./icons/core/chat/finishSpeak.svg'),
'core/chat/quoteFill': () => import('./icons/core/chat/quoteFill.svg'), 'core/chat/quoteFill': () => import('./icons/core/chat/quoteFill.svg'),
'core/chat/chevronDown': () => import('./icons/core/chat/chevronDown.svg'),
'core/chat/chevronUp': () => import('./icons/core/chat/chevronUp.svg'),
'core/chat/quoteSign': () => import('./icons/core/chat/quoteSign.svg'), 'core/chat/quoteSign': () => import('./icons/core/chat/quoteSign.svg'),
'core/chat/recordFill': () => import('./icons/core/chat/recordFill.svg'), 'core/chat/recordFill': () => import('./icons/core/chat/recordFill.svg'),
'core/chat/sendFill': () => import('./icons/core/chat/sendFill.svg'), 'core/chat/sendFill': () => import('./icons/core/chat/sendFill.svg'),
......
<svg viewBox="0 0 19 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.237 6.21967C4.5299 5.92678 5.00477 5.92678 5.29766 6.21967L9.26733 10.1893L13.237 6.21967C13.5299 5.92678 14.0048 5.92678 14.2977 6.21967C14.5906 6.51256 14.5906 6.98744 14.2977 7.28033L9.79766 11.7803C9.50477 12.0732 9.0299 12.0732 8.737 11.7803L4.237 7.28033C3.94411 6.98744 3.94411 6.51256 4.237 6.21967Z" />
</svg>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.96967 11.7803C4.26256 12.0732 4.73744 12.0732 5.03033 11.7803L9 7.81066L12.9697 11.7803C13.2626 12.0732 13.7374 12.0732 14.0303 11.7803C14.3232 11.4874 14.3232 11.0126 14.0303 10.7197L9.53033 6.21967C9.23744 5.92678 8.76256 5.92678 8.46967 6.21967L3.96967 10.7197C3.67678 11.0126 3.67678 11.4874 3.96967 11.7803Z" fill="#667085"/>
</svg>
import { Box, BoxProps, Card, Flex } from '@chakra-ui/react'; import { Box, BoxProps, Card, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react'; import React, { useMemo, useTransition } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController'; import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar'; import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants'; import { MessageCardStyle } from '../constants';
...@@ -11,7 +11,10 @@ import FilesBlock from './FilesBox'; ...@@ -11,7 +11,10 @@ import FilesBlock from './FilesBox';
import { ChatBoxContext } from '../Provider'; import { ChatBoxContext } from '../Provider';
import { useContextSelector } from 'use-context-selector'; import { useContextSelector } from 'use-context-selector';
import AIResponseBox from '../../../components/AIResponseBox'; import AIResponseBox from '../../../components/AIResponseBox';
import { useCopyData } from '@/web/common/hooks/useCopyData';
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useTranslation } from 'next-i18next';
const colorMap = { const colorMap = {
[ChatStatusEnum.loading]: { [ChatStatusEnum.loading]: {
bg: 'myGray.100', bg: 'myGray.100',
...@@ -62,9 +65,11 @@ const ChatItem = ({ ...@@ -62,9 +65,11 @@ const ChatItem = ({
bg: 'myGray.50' bg: 'myGray.50'
}; };
const { t } = useTranslation();
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting); const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const { chat } = chatControllerProps; const { chat } = chatControllerProps;
const { copyData } = useCopyData();
const chatText = useMemo(() => formatChatValue2InputType(chat.value).text || '', [chat.value]);
const ContentCard = useMemo(() => { const ContentCard = useMemo(() => {
if (type === 'Human') { if (type === 'Human') {
const { text, files = [] } = formatChatValue2InputType(chat.value); const { text, files = [] } = formatChatValue2InputType(chat.value);
...@@ -148,6 +153,28 @@ const ChatItem = ({ ...@@ -148,6 +153,28 @@ const ChatItem = ({
> >
{ContentCard} {ContentCard}
{children} {children}
{/* 对话框底部的复制按钮 */}
{type == ChatRoleEnum.AI && (!isChatting || (isChatting && !isLastChild)) && (
<Box
position={'absolute'}
bottom={0}
right={[0, -2]}
color={'myGray.400'}
transform={'translateX(100%)'}
>
<MyTooltip label={t('common.Copy')}>
<MyIcon
w={'14px'}
cursor="pointer"
p="5px"
bg="white"
name={'copy'}
_hover={{ color: 'primary.600' }}
onClick={() => copyData(chatText)}
/>
</MyTooltip>
</Box>
)}
</Card> </Card>
</Box> </Box>
</> </>
......
import React, { useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d'; import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d'; import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
import { Flex, useDisclosure, useTheme, Box } from '@chakra-ui/react'; import { Flex, useDisclosure, Box, Collapse } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type'; import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
...@@ -13,6 +13,7 @@ import ChatBoxDivider from '@/components/core/chat/Divider'; ...@@ -13,6 +13,7 @@ import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools'; import { strIsLink } from '@fastgpt/global/common/string/tools';
import MyIcon from '@fastgpt/web/components/common/Icon'; import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useSize } from 'ahooks';
const QuoteModal = dynamic(() => import('./QuoteModal')); const QuoteModal = dynamic(() => import('./QuoteModal'));
const ContextModal = dynamic(() => import('./ContextModal')); const ContextModal = dynamic(() => import('./ContextModal'));
...@@ -28,9 +29,9 @@ const ResponseTags = ({ ...@@ -28,9 +29,9 @@ const ResponseTags = ({
flowResponses?: ChatHistoryItemResType[]; flowResponses?: ChatHistoryItemResType[];
showDetail: boolean; showDetail: boolean;
}) => { }) => {
const theme = useTheme();
const { isPc } = useSystem(); const { isPc } = useSystem();
const { t } = useTranslation(); const { t } = useTranslation();
const quoteListRef = React.useRef<HTMLDivElement>(null);
const [quoteModalData, setQuoteModalData] = useState<{ const [quoteModalData, setQuoteModalData] = useState<{
rawSearch: SearchDataResponseItemType[]; rawSearch: SearchDataResponseItemType[];
metadata?: { metadata?: {
...@@ -39,6 +40,8 @@ const ResponseTags = ({ ...@@ -39,6 +40,8 @@ const ResponseTags = ({
sourceName: string; sourceName: string;
}; };
}>(); }>();
const [isOverflow, setIsOverflow] = useState<boolean>(true);
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
const [contextModalData, setContextModalData] = const [contextModalData, setContextModalData] =
useState<DispatchNodeResponseType['historyPreview']>(); useState<DispatchNodeResponseType['historyPreview']>();
const { const {
...@@ -47,6 +50,13 @@ const ResponseTags = ({ ...@@ -47,6 +50,13 @@ const ResponseTags = ({
onClose: onCloseWholeModal onClose: onCloseWholeModal
} = useDisclosure(); } = useDisclosure();
const quoteListSize = useSize(quoteListRef);
useEffect(() => {
setIsOverflow(
quoteListRef.current ? quoteListRef.current.scrollHeight > (isPc ? 50 : 55) : true
);
}, [isOverflow, quoteListSize]);
const { const {
llmModuleAccount, llmModuleAccount,
quoteList = [], quoteList = [],
...@@ -64,7 +74,6 @@ const ResponseTags = ({ ...@@ -64,7 +74,6 @@ const ResponseTags = ({
.flat(); .flat();
const chatData = flatResponse.find(isLLMNode); const chatData = flatResponse.find(isLLMNode);
const quoteList = flatResponse const quoteList = flatResponse
.filter((item) => item.moduleType === FlowNodeTypeEnum.datasetSearchNode) .filter((item) => item.moduleType === FlowNodeTypeEnum.datasetSearchNode)
.map((item) => item.quoteList) .map((item) => item.quoteList)
...@@ -80,7 +89,6 @@ const ResponseTags = ({ ...@@ -80,7 +89,6 @@ const ResponseTags = ({
}, },
{} {}
); );
return { return {
llmModuleAccount: flatResponse.filter(isLLMNode).length, llmModuleAccount: flatResponse.filter(isLLMNode).length,
quoteList, quoteList,
...@@ -102,44 +110,103 @@ const ResponseTags = ({ ...@@ -102,44 +110,103 @@ const ResponseTags = ({
<> <>
{sourceList.length > 0 && ( {sourceList.length > 0 && (
<> <>
<ChatBoxDivider icon="core/chat/quoteFill" text={t('common:core.chat.Quote')} /> <Flex justifyContent={'space-between'} alignItems={'center'}>
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2}> <Box width={'100%'}>
{sourceList.map((item) => ( <ChatBoxDivider icon="core/chat/quoteFill" text={t('common:core.chat.Quote')} />{' '}
<MyTooltip key={item.collectionId} label={t('common:core.chat.quote.Read Quote')}> </Box>
{quoteFolded && isOverflow && (
<MyIcon
_hover={{ color: 'primary.500', cursor: 'pointer' }}
name="core/chat/chevronDown"
w={'14px'}
onClick={() => setQuoteFolded(!quoteFolded)}
/>
)}
</Flex>
<Flex alignItems={'center'} flexWrap={'wrap'} gap={2} position={'relative'}>
{
<Collapse
startingHeight={isPc ? '50px' : '55px'}
in={(!quoteFolded && isOverflow) || !isOverflow}
>
<Flex <Flex
ref={quoteListRef}
alignItems={'center'} alignItems={'center'}
fontSize={'xs'}
border={theme.borders.sm}
py={1.5}
px={2}
borderRadius={'sm'}
_hover={{
'.controller': {
display: 'flex'
}
}}
overflow={'hidden'}
position={'relative'} position={'relative'}
cursor={'pointer'} flexWrap={'wrap'}
onClick={(e) => { gap={2}
e.stopPropagation(); height={quoteFolded && isOverflow ? ['55px', '50px'] : 'auto'}
setQuoteModalData({ overflow={'hidden'}
rawSearch: quoteList, _after={
metadata: { quoteFolded && isOverflow
collectionId: item.collectionId, ? {
sourceId: item.sourceId, content: '""',
sourceName: item.sourceName position: 'absolute',
} zIndex: 2,
}); bottom: 0,
}} left: 0,
width: '100%',
height: '50%',
background:
'linear-gradient(to bottom, rgba(247,247,247,0), rgba(247, 247, 247, 0.91))',
pointerEvents: 'none'
}
: {}
}
> >
<MyIcon name={item.icon as any} mr={1} flexShrink={0} w={'12px'} /> {sourceList.map((item) => {
<Box className="textEllipsis3" wordBreak={'break-all'} flex={'1 0 0'}> return (
{item.sourceName} <MyTooltip key={item.collectionId} label={t('core.chat.quote.Read Quote')}>
</Box> <Flex
alignItems={'center'}
fontSize={'xs'}
border={'sm'}
py={1.5}
px={2}
borderRadius={'sm'}
_hover={{
'.controller': {
display: 'flex'
}
}}
overflow={'hidden'}
position={'relative'}
cursor={'pointer'}
onClick={(e) => {
e.stopPropagation();
setQuoteModalData({
rawSearch: quoteList,
metadata: {
collectionId: item.collectionId,
sourceId: item.sourceId,
sourceName: item.sourceName
}
});
}}
>
<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>
</MyTooltip>
);
})}
{isOverflow && !quoteFolded && (
<MyIcon
position={'absolute'}
bottom={0}
right={0}
_hover={{ color: 'primary.500', cursor: 'pointer' }}
name="core/chat/chevronUp"
w={'14px'}
onClick={() => setQuoteFolded(!quoteFolded)}
/>
)}
</Flex> </Flex>
</MyTooltip> </Collapse>
))} }
</Flex> </Flex>
</> </>
)} )}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment