feat: add chat history time label (#3024)

* feat:add chat and logs time

* feat: add chat history time label

* code perf

* code perf

---------

Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>
This commit is contained in:
papapatrick 2024-11-04 10:52:58 +08:00 committed by archer
parent 469858877e
commit 727bd7144c
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
17 changed files with 289 additions and 174 deletions

View File

@ -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 */

View File

@ -126,6 +126,7 @@ export type ChatSiteItemType = (UserChatItemType | SystemChatItemType | AIChatIt
moduleName?: string;
ttsBuffer?: Uint8Array;
responseData?: ChatHistoryItemResType[];
time?: Date;
} & ChatBoxInputType &
ResponseTagItemType;

View File

@ -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}}"
}

View File

@ -18,6 +18,9 @@
"FAQ.switch_package_a": "套餐使用规则为优先使用更高级的套餐,因此,购买的新套餐若比当前套餐更高级,则新套餐立即生效:否则将继续使用当前套餐。",
"FAQ.switch_package_q": "是否切换订阅套餐?",
"Folder": "文件夹",
"just_now": "刚刚",
"yesterday": "昨天",
"yesterday_detail_time": "昨天 {{time}}",
"Login": "登录",
"Move": "移动",
"Name": "名称",

View File

@ -34,6 +34,7 @@ export type ChatProviderProps = OutLinkChatAuthProps & {
// not chat test params
chatId?: string;
isLog?: boolean;
};
type useChatStoreType = OutLinkChatAuthProps &

View File

@ -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 (
<>
<Box
_hover={{
'& .time-label': {
display: 'block'
}
}}
>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
<Flex w={'100%'} alignItems={'flex-end'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<Flex order={styleMap.order} ml={styleMap.ml} align={'center'} gap={'0.62rem'}>
{chat.time && type === ChatRoleEnum.Human && (isPc || isLog) && (
<Box
className={'time-label'}
fontSize={styleMap.fontSize}
color={styleMap.color}
fontWeight={styleMap.fontWeight}
display={isLog ? 'block' : 'none'}
>
{t(formatTimeToChatItemTime(chat.time) as any, {
time: dayjs(chat.time).format('HH:mm')
}).replace('#', ':')}
</Box>
)}
<ChatController {...props} isLastChild={isLastChild} />
</Box>
</Flex>
)}
<ChatAvatar src={avatar} type={type} />
@ -290,7 +320,7 @@ const ChatItem = (props: Props) => {
</Card>
</Box>
))}
</>
</Box>
);
};

View File

@ -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 (
<Box w={'100%'} fontSize={'mini'} textAlign={'center'} color={'myGray.500'} fontWeight={'400'}>
{t(formatTimeToChatItemTime(time) as any, {
time: dayjs(time).format('HH#mm')
}).replace('#', ':')}
</Box>
);
};
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 */}
<Box id={'history'}>
{chatHistories.map((item, index) => (
<Box key={item.dataId} py={5}>
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
/>
)}
{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 && <ChatTimeBox time={item.time} />}
<Box key={item.dataId} py={6}>
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={appAvatar}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
showDetail={!shareId && !teamId}
historyItem={item}
/>
/>
)}
{item.obj === ChatRoleEnum.AI && (
<>
<ChatItem
type={item.obj}
avatar={appAvatar}
chat={item}
isLastChild={index === chatHistories.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
showDetail={!shareId && !teamId}
historyItem={item}
/>
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('common:core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={i}>
<MyTooltip
label={t('common:core.app.feedback.close custom feedback')}
>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('common:core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={i}>
<MyTooltip
label={t('common:core.app.feedback.close custom feedback')}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box fontSize={'sm'}>
<ChatBoxDivider
icon="core/app/markLight"
text={t('common:core.chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre-wrap'}>
<Box color={'black'}>{item.adminFeedback.q}</Box>
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
</Box>
)}
</ChatItem>
</>
)}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box fontSize={'sm'}>
<ChatBoxDivider
icon="core/app/markLight"
text={t('common:core.chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre-wrap'}>
<Box color={'black'}>{item.adminFeedback.q}</Box>
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
</Box>
</Box>
)}
</ChatItem>
</>
)}
</Box>
</>
))}
</Box>
</Box>

View File

@ -43,7 +43,7 @@ const InformTable = () => {
<Flex alignItems={'center'}>
<Box fontWeight={'bold'}>{item.title}</Box>
<Box ml={2} color={'myGray.500'} flex={'1 0 0'}>
({formatTimeToChatTime(item.time)})
({t(formatTimeToChatTime(item.time) as any).replace('#', ':')})
</Box>
{!item.read && (
<Button

View File

@ -67,13 +67,13 @@ async function handler(
})();
const fieldMap = {
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback ${
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`,
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${
shareChat?.responseDetail || isPlugin ? `${DispatchNodeResponseKeyEnum.nodeResponse}` : ''
} `,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback ${DispatchNodeResponseKeyEnum.nodeResponse}`
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`
};
const { total, histories } = await getChatItems({

View File

@ -180,6 +180,7 @@ const DetailLogsModal = ({
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
isLog={true}
/>
)}
</Box>

View File

@ -147,7 +147,7 @@ const FeiShu = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@ -150,7 +150,9 @@ const Share = ({ appId }: { appId: string; type: PublishChannelEnum }) => {
</>
)}
<Td>
{item.lastTime ? formatTimeToChatTime(item.lastTime) : t('common:common.Un used')}
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>
<Button

View File

@ -150,7 +150,7 @@ const OffiAccount = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@ -126,7 +126,7 @@ const Wecom = ({ appId }: { appId: string }) => {
)}
<Td>
{item.lastTime
? t(formatTimeToChatTime(item.lastTime) as any)
? t(formatTimeToChatTime(item.lastTime) as any).replace('#', ':')
: t('common:common.Un used')}
</Td>
<Td display={'flex'} alignItems={'center'}>

View File

@ -249,7 +249,9 @@ const ListItem = () => {
{isPc && (
<HStack spacing={0.5} className="time">
<MyIcon name={'history'} w={'0.85rem'} color={'myGray.400'} />
<Box color={'myGray.500'}>{formatTimeToChatTime(app.updateTime)}</Box>
<Box color={'myGray.500'}>
{t(formatTimeToChatTime(app.updateTime) as any).replace('#', ':')}
</Box>
</HStack>
)}
{(AppFolderTypeList.includes(app.type)

View File

@ -13,12 +13,14 @@ import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useContextSelector } from 'use-context-selector';
import { ChatContext } from '@/web/core/chat/context/chatContext';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { formatTimeToChatTime } from '@fastgpt/global/common/string/time';
type HistoryItemType = {
id: string;
title: string;
customTitle?: string;
top?: boolean;
updateTime: Date;
};
const ChatHistorySlider = ({
@ -59,11 +61,18 @@ const ChatHistorySlider = ({
const concatHistory = useMemo(() => {
const formatHistories: HistoryItemType[] = histories.map((item) => {
return { id: item.chatId, title: item.title, customTitle: item.customTitle, top: item.top };
return {
id: item.chatId,
title: item.title,
customTitle: item.customTitle,
top: item.top,
updateTime: item.updateTime
};
});
const newChat: HistoryItemType = {
id: activeChatId,
title: t('common:core.chat.New Chat')
title: t('common:core.chat.New Chat'),
updateTime: new Date()
};
const activeChat = histories.find((item) => item.chatId === activeChatId);
@ -188,7 +197,10 @@ const ChatHistorySlider = ({
_hover={{
bg: 'myGray.50',
'& .more': {
visibility: 'visible'
display: 'block'
},
'& .time': {
display: isPc ? 'none' : 'block'
}
}}
bg={item.top ? '#E6F6F6 !important' : ''}
@ -214,69 +226,80 @@ const ChatHistorySlider = ({
{item.customTitle || item.title}
</Box>
{!!item.id && (
<Box className="more" visibility={['visible', 'hidden']}>
<MyMenu
Button={
<IconButton
size={'xs'}
variant={'whiteBase'}
icon={<MyIcon name={'more'} w={'14px'} p={1} />}
aria-label={''}
/>
}
menuList={[
{
children: [
...(onSetHistoryTop
? [
{
label: item.top
? t('common:core.chat.Unpin')
: t('common:core.chat.Pin'),
icon: 'core/chat/setTopLight',
onClick: () => {
onSetHistoryTop({
chatId: item.id,
top: !item.top
});
}
}
]
: []),
...(onSetCustomTitle
? [
{
label: t('common:common.Custom Title'),
icon: 'common/customTitleLight',
onClick: () => {
onOpenModal({
defaultVal: item.customTitle || item.title,
onSuccess: (e) =>
onSetCustomTitle({
chatId: item.id,
title: e
})
});
}
}
]
: []),
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
onDelHistory({ chatId: item.id });
if (item.id === activeChatId) {
onChangeChatId();
}
},
type: 'danger'
}
]
<Flex gap={2} alignItems={'center'}>
<Box
className="time"
display={'block'}
fontWeight={'400'}
fontSize={'mini'}
color={'myGray.500'}
>
{t(formatTimeToChatTime(item.updateTime) as any).replace('#', ':')}
</Box>
<Box className="more" display={['block', 'none']}>
<MyMenu
Button={
<IconButton
size={'xs'}
variant={'whiteBase'}
icon={<MyIcon name={'more'} w={'14px'} p={1} />}
aria-label={''}
/>
}
]}
/>
</Box>
menuList={[
{
children: [
...(onSetHistoryTop
? [
{
label: item.top
? t('common:core.chat.Unpin')
: t('common:core.chat.Pin'),
icon: 'core/chat/setTopLight',
onClick: () => {
onSetHistoryTop({
chatId: item.id,
top: !item.top
});
}
}
]
: []),
...(onSetCustomTitle
? [
{
label: t('common:common.Custom Title'),
icon: 'common/customTitleLight',
onClick: () => {
onOpenModal({
defaultVal: item.customTitle || item.title,
onSuccess: (e) =>
onSetCustomTitle({
chatId: item.id,
title: e
})
});
}
}
]
: []),
{
label: t('common:common.Delete'),
icon: 'delete',
onClick: () => {
onDelHistory({ chatId: item.id });
if (item.id === activeChatId) {
onChangeChatId();
}
},
type: 'danger'
}
]
}
]}
/>
</Box>
</Flex>
)}
</Flex>
))}

View File

@ -382,9 +382,7 @@ const TestHistories = React.memo(function TestHistories({
{item.text}
</Box>
<Box flex={'0 0 70px'}>
{formatTimeToChatTime(item.time).includes('.')
? t(formatTimeToChatTime(item.time) as any)
: formatTimeToChatTime(item.time)}
{t(formatTimeToChatTime(item.time) as any).replace('#', ':')}
</Box>
<MyTooltip label={t('common:core.dataset.test.delete test history')}>
<Box w={'14px'} h={'14px'}>