mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-24 22:03:54 +00:00
Chat-perf (#2117)
* feat: 新增对话框底部复制按钮 * fix: 对话框底部复制按钮定位问题 * feat: 过长引用折叠功能 * merge: 合并主仓库代码 * refactor: 删除不必要代码 * doc: 文档补充的部分修改成英文
This commit is contained in:
17
dev.md
17
dev.md
@@ -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) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 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 = () => {
|
|||||||
|
|
||||||
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 = {
|
|||||||
|
|
||||||
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
|
|||||||
# 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 = {
|
|||||||
'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'),
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 486 B |
@@ -1,5 +1,5 @@
|
|||||||
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';
|
|||||||
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 = ({
|
|||||||
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 = ({
|
|||||||
>
|
>
|
||||||
{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>
|
||||||
</>
|
</>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
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';
|
|||||||
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 = ({
|
|||||||
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 = ({
|
|||||||
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 = ({
|
|||||||
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 = ({
|
|||||||
.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 = ({
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
llmModuleAccount: flatResponse.filter(isLLMNode).length,
|
llmModuleAccount: flatResponse.filter(isLLMNode).length,
|
||||||
quoteList,
|
quoteList,
|
||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
Reference in New Issue
Block a user