From 727bd7144c9c6822c88988b02a135362cc41a17a Mon Sep 17 00:00:00 2001 From: papapatrick <109422393+Patrickill@users.noreply.github.com> Date: Mon, 4 Nov 2024 10:52:58 +0800 Subject: [PATCH] feat: add chat history time label (#3024) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat:add chat and logs time * feat: add chat history time label * code perf * code perf --------- Co-authored-by: 勤劳上班的卑微小张 --- packages/global/common/string/time.ts | 41 +++- packages/global/core/chat/type.d.ts | 1 + packages/web/i18n/en/common.json | 8 +- packages/web/i18n/zh/common.json | 3 + .../chat/ChatContainer/ChatBox/Provider.tsx | 1 + .../ChatBox/components/ChatItem.tsx | 52 +++-- .../core/chat/ChatContainer/ChatBox/index.tsx | 177 ++++++++++-------- .../pages/account/components/InformTable.tsx | 2 +- .../api/core/chat/getPaginationRecords.ts | 6 +- .../components/Logs/DetailLogsModal.tsx | 1 + .../components/Publish/FeiShu/index.tsx | 2 +- .../detail/components/Publish/Link/index.tsx | 4 +- .../components/Publish/OffiAccount/index.tsx | 2 +- .../detail/components/Publish/Wecom/index.tsx | 2 +- .../src/pages/app/list/components/List.tsx | 4 +- .../chat/components/ChatHistorySlider.tsx | 153 ++++++++------- .../pages/dataset/detail/components/Test.tsx | 4 +- 17 files changed, 289 insertions(+), 174 deletions(-) diff --git a/packages/global/common/string/time.ts b/packages/global/common/string/time.ts index 2ae2980fd..2c0ecc4b3 100644 --- a/packages/global/common/string/time.ts +++ b/packages/global/common/string/time.ts @@ -2,6 +2,7 @@ import dayjs from 'dayjs'; import cronParser from 'cron-parser'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; +import { i18nT } from '../../../web/i18n/utils'; dayjs.extend(utc); dayjs.extend(timezone); @@ -23,31 +24,51 @@ export const formatTimeToChatTime = (time: Date) => { // 如果传入时间小于60秒,返回刚刚 if (now.diff(target, 'second') < 60) { - return '刚刚'; + return i18nT('common:just_now'); } // 如果时间是今天,展示几时:几分 + //用#占位,i18n生效后replace成: if (now.isSame(target, 'day')) { - return target.format('HH : mm'); + return 'common:' + target.format('HH#mm'); } // 如果是昨天,展示昨天 if (now.subtract(1, 'day').isSame(target, 'day')) { - return '昨天'; - } - - // 如果是前天,展示前天 - if (now.subtract(2, 'day').isSame(target, 'day')) { - return '前天'; + return i18nT('common:yesterday'); } // 如果是今年,展示某月某日 if (now.isSame(target, 'year')) { - return target.format('MM/DD'); + return target.format('MM-DD'); } // 如果是更久之前,展示某年某月某日 - return target.format('YYYY/M/D'); + return target.format('YYYY-M-D'); +}; + +export const formatTimeToChatItemTime = (time: Date) => { + const now = dayjs(); + const target = dayjs(time); + const detailTime = target.format('HH#mm'); + + // 如果时间是今天,展示几时:几分 + if (now.isSame(target, 'day')) { + return 'common:' + detailTime; + } + + // 如果是昨天,展示昨天+几时:几分 + if (now.subtract(1, 'day').isSame(target, 'day')) { + return i18nT('common:yesterday_detail_time'); + } + + // 如果是今年,展示某月某日+几时:几分 + if (now.isSame(target, 'year')) { + return target.format('MM-DD') + ' ' + detailTime; + } + + // 如果是更久之前,展示某年某月某日+几时:几分 + return target.format('YYYY-M-D') + ' ' + detailTime; }; /* cron time parse */ diff --git a/packages/global/core/chat/type.d.ts b/packages/global/core/chat/type.d.ts index d606efb8d..3d16c2653 100644 --- a/packages/global/core/chat/type.d.ts +++ b/packages/global/core/chat/type.d.ts @@ -126,6 +126,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt moduleName?: string; ttsBuffer?: Uint8Array; responseData?: ChatHistoryItemResType[]; + time?: Date; } & ChatBoxInputType & ResponseTagItemType; diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 8626a21ad..f7040562d 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -69,8 +69,12 @@ "code_error.system_error.community_version_num_limit": "Exceeded Open Source Version Limit, Please Upgrade to Commercial Version: https://tryfastgpt.ai", "code_error.team_error.ai_points_not_enough": "Insufficient AI Points", "code_error.team_error.app_amount_not_enough": "Application Limit Reached", + "code_error.team_error.cannot_delete_default_group": "Cannot delete default group", "code_error.team_error.dataset_amount_not_enough": "Dataset Limit Reached", "code_error.team_error.dataset_size_not_enough": "Insufficient Dataset Capacity, Please Expand", + "code_error.team_error.group_name_duplicate": "Duplicate group name", + "code_error.team_error.group_name_empty": "Group name cannot be empty", + "code_error.team_error.group_not_exist": "Group does not exist", "code_error.team_error.over_size": "error.team.overSize", "code_error.team_error.plugin_amount_not_enough": "Plugin Limit Reached", "code_error.team_error.re_rank_not_enough": "Unauthorized to Use Re-Rank", @@ -1200,5 +1204,7 @@ "user.type": "Type", "verification": "Verification", "xx_search_result": "{{key}} Search Results", - "yes": "Yes" + "yes": "Yes", + "yesterday": "yesterday", + "yesterday_detail_time": "Yesterday {{time}}" } diff --git a/packages/web/i18n/zh/common.json b/packages/web/i18n/zh/common.json index 39762c250..1b669e466 100644 --- a/packages/web/i18n/zh/common.json +++ b/packages/web/i18n/zh/common.json @@ -18,6 +18,9 @@ "FAQ.switch_package_a": "套餐使用规则为优先使用更高级的套餐,因此,购买的新套餐若比当前套餐更高级,则新套餐立即生效:否则将继续使用当前套餐。", "FAQ.switch_package_q": "是否切换订阅套餐?", "Folder": "文件夹", + "just_now": "刚刚", + "yesterday": "昨天", + "yesterday_detail_time": "昨天 {{time}}", "Login": "登录", "Move": "移动", "Name": "名称", diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx index d178992c9..31b470974 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx @@ -34,6 +34,7 @@ export type ChatProviderProps = OutLinkChatAuthProps & { // not chat test params chatId?: string; + isLog?: boolean; }; type useChatStoreType = OutLinkChatAuthProps & diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx index 91f89439e..33d382418 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/components/ChatItem.tsx @@ -1,5 +1,5 @@ import { Box, BoxProps, Card, Flex } from '@chakra-ui/react'; -import React, { useMemo } from 'react'; +import React, { useMemo, useRef } from 'react'; import ChatController, { type ChatControllerProps } from './ChatController'; import ChatAvatar from './ChatAvatar'; import { MessageCardStyle } from '../constants'; @@ -22,6 +22,9 @@ import { useTranslation } from 'next-i18next'; import { AIChatItemValueItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type'; import { CodeClassNameEnum } from '@/components/Markdown/utils'; import { isEqual } from 'lodash'; +import { useSystem } from '@fastgpt/web/hooks/useSystem'; +import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time'; +import dayjs from 'dayjs'; const colorMap = { [ChatStatusEnum.loading]: { @@ -110,8 +113,10 @@ const AIContentCard = React.memo(function AIContentCard({ const ChatItem = (props: Props) => { const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props; - const styleMap: BoxProps = - type === ChatRoleEnum.Human + const { isPc } = useSystem(); + + const styleMap: BoxProps = { + ...(type === ChatRoleEnum.Human ? { order: 0, borderRadius: '8px 0 8px 8px', @@ -125,10 +130,16 @@ const ChatItem = (props: Props) => { justifyContent: 'flex-start', textAlign: 'left', bg: 'myGray.50' - }; - + }), + fontSize: 'mini', + fontWeight: '400', + color: 'myGray.500' + }; const { t } = useTranslation(); - const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting); + + const { isChatting, isLog } = useContextSelector(ChatBoxContext, (v) => { + return { isChatting: v.isChatting, isLog: v.isLog }; + }); const { copyData } = useCopyData(); @@ -196,13 +207,32 @@ const ChatItem = (props: Props) => { }, [chat.obj, chat.value, isChatting]); return ( - <> + {/* control icon */} - + {isChatting && type === ChatRoleEnum.AI && isLastChild ? null : ( - + + {chat.time && type === ChatRoleEnum.Human && (isPc || isLog) && ( + + {t(formatTimeToChatItemTime(chat.time) as any, { + time: dayjs(chat.time).format('HH:mm') + }).replace('#', ':')} + + )} - + )} @@ -290,7 +320,7 @@ const ChatItem = (props: Props) => { ))} - + ); }; diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx index a98d1a3cd..78855b298 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx @@ -67,6 +67,8 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem'; import { useCreation, useMemoizedFn, useThrottleFn } from 'ahooks'; import MyIcon from '@fastgpt/web/components/common/Icon'; import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils'; +import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time'; +import dayjs from 'dayjs'; const ResponseTags = dynamic(() => import('./components/ResponseTags')); const FeedbackModal = dynamic(() => import('./components/FeedbackModal')); @@ -108,6 +110,18 @@ type Props = OutLinkChatAuthProps & onDelMessage?: (e: { contentId: string }) => void; }; +const ChatTimeBox = ({ time }: { time: Date }) => { + const { t } = useTranslation(); + + return ( + + {t(formatTimeToChatItemTime(time) as any, { + time: dayjs(time).format('HH#mm') + }).replace('#', ':')} + + ); +}; + const ChatBox = ( { feedbackType = FeedbackTypeEnum.hidden, @@ -436,6 +450,7 @@ const ChatBox = ( { dataId: getNanoid(24), obj: ChatRoleEnum.Human, + time: new Date(), value: [ ...files.map((file) => ({ type: ChatItemValueTypeEnum.file, @@ -521,6 +536,7 @@ const ChatBox = ( return { ...item, status: ChatStatusEnum.finish, + time: new Date(), responseData: item.responseData ? mergeChatResponseData([...item.responseData, ...responseData]) : responseData @@ -562,6 +578,7 @@ const ChatBox = ( if (index !== state.length - 1) return item; return { ...item, + time: new Date(), status: ChatStatusEnum.finish }; }) @@ -877,88 +894,98 @@ const ChatBox = ( {/* chat history */} {chatHistories.map((item, index) => ( - - {item.obj === ChatRoleEnum.Human && ( - - )} - {item.obj === ChatRoleEnum.AI && ( - <> + <> + {/* 并且时间和上一条的time相差超过十分钟 */} + {index !== 0 && + item.time && + chatHistories[index - 1].time !== undefined && + new Date(item.time).getTime() - + new Date(chatHistories[index - 1].time!).getTime() > + 10 * 60 * 1000 && } + + + {item.obj === ChatRoleEnum.Human && ( - + /> + )} + {item.obj === ChatRoleEnum.AI && ( + <> + + - {/* custom feedback */} - {item.customFeedbacks && item.customFeedbacks.length > 0 && ( - - - {item.customFeedbacks.map((text, i) => ( - - - } + {/* custom feedback */} + {item.customFeedbacks && item.customFeedbacks.length > 0 && ( + + + {item.customFeedbacks.map((text, i) => ( + + - {text} - - - - ))} - - )} - {/* admin mark content */} - {showMarkIcon && item.adminFeedback && ( - - - - {item.adminFeedback.q} - {item.adminFeedback.a} + } + > + {text} + + + + ))} - - )} - - - )} - + )} + {/* admin mark content */} + {showMarkIcon && item.adminFeedback && ( + + + + {item.adminFeedback.q} + {item.adminFeedback.a} + + + )} + + + )} + + ))} diff --git a/projects/app/src/pages/account/components/InformTable.tsx b/projects/app/src/pages/account/components/InformTable.tsx index 101d50209..482d8da9f 100644 --- a/projects/app/src/pages/account/components/InformTable.tsx +++ b/projects/app/src/pages/account/components/InformTable.tsx @@ -43,7 +43,7 @@ const InformTable = () => { {item.title} - ({formatTimeToChatTime(item.time)}) + ({t(formatTimeToChatTime(item.time) as any).replace('#', ':')}) {!item.read && (