From 5bb8658b0df17fe7dce0756108b6563c8607c39b Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 24 Jun 2026 16:19:55 +0800 Subject: [PATCH] fix: use timestamp hook --- .../chat/chat-with-history/chat-wrapper.tsx | 5 +++++ .../base/chat/chat/__tests__/hooks.spec.tsx | 21 ++++++++++++++++++- web/app/components/base/chat/chat/hooks.ts | 7 ++++++- .../chat/embedded-chatbot/chat-wrapper.tsx | 5 +++++ web/hooks/use-timestamp.spec.ts | 19 +++-------------- web/hooks/use-timestamp.ts | 16 +++++--------- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx index 5e685bff284..43f23d4dd98 100644 --- a/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx +++ b/web/app/components/base/chat/chat-with-history/chat-wrapper.tsx @@ -57,6 +57,9 @@ const ChatWrapper = () => { } = useChatWithHistoryContext() const appSourceType = isInstalledApp ? AppSourceType.installedApp : AppSourceType.webApp + const timezone = appSourceType === AppSourceType.webApp + ? Intl.DateTimeFormat().resolvedOptions().timeZone + : undefined // Semantic variable for better code readability const isHistoryConversation = !!currentConversationId @@ -91,6 +94,8 @@ const ChatWrapper = () => { taskId => stopChatMessageResponding('', taskId, appSourceType, appId), clearChatList, setClearChatList, + undefined, + { timezone }, ) const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current const inputDisabled = useMemo(() => { diff --git a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx index 10624f27574..954fdc06a00 100644 --- a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx @@ -6,6 +6,10 @@ import { useParams, usePathname } from '@/next/navigation' import { sseGet, ssePost } from '@/service/base' import { useChat } from '../hooks' +const useTimestampMock = vi.hoisted(() => + vi.fn(() => ({ formatTime: vi.fn().mockReturnValue('10:00 AM') })), +) + vi.mock('@/service/base', () => ({ sseGet: vi.fn(), ssePost: vi.fn(), @@ -31,7 +35,7 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ })) vi.mock('@/hooks/use-timestamp', () => ({ - default: () => ({ formatTime: vi.fn().mockReturnValue('10:00 AM') }), + default: useTimestampMock, })) vi.mock('@/next/navigation', () => ({ @@ -91,6 +95,21 @@ describe('useChat', () => { expect(result.current.suggestedQuestions).toEqual([]) }) + it('should pass timestamp options to timestamp formatter', () => { + renderHook(() => useChat( + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + { timezone: 'UTC' }, + )) + + expect(useTimestampMock).toHaveBeenCalledWith({ timezone: 'UTC' }) + }) + it('should initialize with opening statement and suggested questions', () => { const config = { opening_statement: 'Hello {{name}}', diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 982c408fb63..1743e408553 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -55,6 +55,10 @@ type SendCallback = { isPublicAPI?: boolean } +type UseChatOptions = { + timezone?: string +} + export const useChat = ( config?: ChatConfig, formSettings?: { @@ -66,9 +70,10 @@ export const useChat = ( clearChatList?: boolean, clearChatListCallback?: (state: boolean) => void, initialConversationId?: string, + options: UseChatOptions = {}, ) => { const { t } = useTranslation() - const { formatTime } = useTimestamp() + const { formatTime } = useTimestamp({ timezone: options.timezone }) const conversationIdRef = useRef(initialConversationId ?? '') const initialConversationIdRef = useRef(initialConversationId ?? '') const hasStopRespondedRef = useRef(false) diff --git a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx index 63cecc99df7..ff7af0f25df 100644 --- a/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx +++ b/web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx @@ -81,6 +81,9 @@ const ChatWrapper = () => { opening_statement: currentConversationItem?.introduction || (config as any).opening_statement, } as ChatConfig }, [appParams, currentConversationItem?.introduction]) + const timezone = appSourceType === AppSourceType.webApp + ? Intl.DateTimeFormat().resolvedOptions().timeZone + : undefined const { chatList, handleSend, @@ -98,6 +101,8 @@ const ChatWrapper = () => { taskId => stopChatMessageResponding('', taskId, appSourceType, appId), clearChatList, setClearChatList, + undefined, + { timezone }, ) const inputsFormValue = currentConversationId ? currentConversationInputs : newConversationInputsRef?.current const inputDisabled = useMemo(() => { diff --git a/web/hooks/use-timestamp.spec.ts b/web/hooks/use-timestamp.spec.ts index 0cc065a6e3c..ab8127b88ce 100644 --- a/web/hooks/use-timestamp.spec.ts +++ b/web/hooks/use-timestamp.spec.ts @@ -6,14 +6,6 @@ import { userProfileQueryOptions } from '@/features/account-profile/client' import { createAccountProfileQueryWrapper } from '@/test/account-profile-query' import useTimestamp from './use-timestamp' -const navigationMocks = vi.hoisted(() => ({ - pathname: '/apps', -})) - -vi.mock('@/next/navigation', () => ({ - usePathname: () => navigationMocks.pathname, -})) - vi.mock('@/context/app-context', () => ({ useAppContext: vi.fn(() => ({ userProfile: { @@ -50,10 +42,6 @@ const createEmptyQueryWrapper = () => { } describe('useTimestamp', () => { - beforeEach(() => { - navigationMocks.pathname = '/apps' - }) - describe('formatTime', () => { it('should format unix timestamp correctly', () => { const { result } = renderHook(() => useTimestamp(), { wrapper: createAccountProfileQueryWrapper() }) @@ -96,13 +84,12 @@ describe('useTimestamp', () => { }) }) - it('should not request account profile on public webapp routes', () => { - navigationMocks.pathname = '/chatbot/share-token' + it('should not request account profile when timezone is provided', () => { const { queryClient, wrapper } = createEmptyQueryWrapper() - const { result } = renderHook(() => useTimestamp(), { wrapper }) + const { result } = renderHook(() => useTimestamp({ timezone: 'UTC' }), { wrapper }) - expect(result.current.formatTime(1704132000, 'YYYY')).toBe('2024') + expect(result.current.formatTime(1704132000, 'YYYY-MM-DD HH:mm')).toBe('2024-01-01 18:00') expect(queryClient.isFetching({ queryKey: userProfileQueryOptions().queryKey })).toBe(0) expect(queryClient.getQueryState(userProfileQueryOptions().queryKey)?.fetchStatus).toBe('idle') }) diff --git a/web/hooks/use-timestamp.ts b/web/hooks/use-timestamp.ts index 58496b01403..96eeae9dd52 100644 --- a/web/hooks/use-timestamp.ts +++ b/web/hooks/use-timestamp.ts @@ -5,31 +5,25 @@ import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' import { useCallback } from 'react' import { userProfileQueryOptions } from '@/features/account-profile/client' -import { usePathname } from '@/next/navigation' dayjs.extend(utc) dayjs.extend(timezone) -const PUBLIC_WEBAPP_ROUTE_SEGMENTS = new Set(['agent', 'chat', 'chatbot', 'completion', 'workflow']) - -const isPublicWebAppPath = (pathname: string | null) => { - const segment = pathname?.split('/').find(Boolean) - return segment ? PUBLIC_WEBAPP_ROUTE_SEGMENTS.has(segment) : false +type UseTimestampOptions = { + timezone?: string } const getBrowserTimezone = () => { return Intl.DateTimeFormat().resolvedOptions().timeZone } -const useTimestamp = () => { - const pathname = usePathname() - const shouldUseAccountTimezone = !isPublicWebAppPath(pathname) +const useTimestamp = ({ timezone: timezoneOverride }: UseTimestampOptions = {}) => { const { data: accountTimezone } = useQuery({ ...userProfileQueryOptions(), select: data => data.profile.timezone ?? undefined, - enabled: shouldUseAccountTimezone, + enabled: timezoneOverride === undefined, }) - const resolvedTimezone = accountTimezone ?? getBrowserTimezone() + const resolvedTimezone = timezoneOverride ?? accountTimezone ?? getBrowserTimezone() const formatTime = useCallback((value: number, format: string) => { return dayjs.unix(value).tz(resolvedTimezone).format(format)