diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 18aaf0daee..90d822b0cb 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -209,6 +209,7 @@ export default function useChatFunctions({ text, sender: 'User', clientTimestamp: new Date().toLocaleString('sv').replace(' ', 'T'), + clientTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone, isCreatedByUser: true, parentMessageId, conversationId, diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 81bc89cac4..e66ff9eb9b 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -403,6 +403,7 @@ export async function initializeAgent( agent.instructions = replaceSpecialVars({ text: agent.instructions, user: req.user ? (req.user as unknown as TUser) : null, + timezone: req.body?.clientTimezone, }); } diff --git a/packages/api/src/types/http.ts b/packages/api/src/types/http.ts index c304e9089e..1a3cab3d49 100644 --- a/packages/api/src/types/http.ts +++ b/packages/api/src/types/http.ts @@ -16,6 +16,7 @@ export type RequestBody = { model?: string; key?: string; endpointOption?: Partial; + clientTimezone?: string; }; export type ServerRequest = Request & { diff --git a/packages/data-provider/specs/parsers.spec.ts b/packages/data-provider/specs/parsers.spec.ts index 83c3500922..e30a5b7298 100644 --- a/packages/data-provider/specs/parsers.spec.ts +++ b/packages/data-provider/specs/parsers.spec.ts @@ -7,13 +7,13 @@ import type { TUser, TConversation } from '../src/types'; // Mock dayjs module with consistent date/time values regardless of environment jest.mock('dayjs', () => { - const mockDayjs = () => ({ + const makeMock = (offset: string) => ({ format: (format: string) => { if (format === 'YYYY-MM-DD') { return '2024-04-29'; } if (format === 'YYYY-MM-DD HH:mm:ss Z') { - return '2024-04-29 12:34:56 -04:00'; + return `2024-04-29 12:34:56 ${offset}`; } if (format === 'dddd') { return 'Monday'; @@ -23,8 +23,19 @@ jest.mock('dayjs', () => { ); }, toISOString: () => '2024-04-29T16:34:56.000Z', + isValid: () => true, + tz: (timezone: string) => { + if (timezone === 'America/New_York') { + return makeMock('-04:00'); + } + if (timezone === 'Asia/Tokyo') { + return makeMock('+09:00'); + } + return { isValid: () => false }; + }, }); + const mockDayjs = () => makeMock('-04:00'); mockDayjs.extend = jest.fn(); return mockDayjs; @@ -126,6 +137,56 @@ describe('replaceSpecialVars', () => { expect(result).toContain('2024-04-29T16:34:56.000Z'); // iso_datetime expect(result).toContain('Test User'); // current_user }); + + test('should use provided timezone for date formatting', () => { + const result = replaceSpecialVars({ + text: 'Now is {{current_datetime}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('Now is 2024-04-29 12:34:56 +09:00 (Monday)'); + }); + + test('should fall back to default when no timezone is provided', () => { + const result = replaceSpecialVars({ + text: 'Now is {{current_datetime}}', + }); + expect(result).toBe('Now is 2024-04-29 12:34:56 -04:00 (Monday)'); + }); + + test('should fall back to default for invalid timezone', () => { + const result = replaceSpecialVars({ + text: 'Now is {{current_datetime}}', + timezone: 'Invalid/Timezone', + }); + expect(result).toBe('Now is 2024-04-29 12:34:56 -04:00 (Monday)'); + }); + + test('should apply timezone to {{current_date}} formatting', () => { + const result = replaceSpecialVars({ + text: 'Today is {{current_date}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('Today is 2024-04-29 (Monday)'); + }); + + test('should not affect {{iso_datetime}} regardless of timezone', () => { + const result = replaceSpecialVars({ + text: 'ISO: {{iso_datetime}}', + timezone: 'Asia/Tokyo', + }); + expect(result).toBe('ISO: 2024-04-29T16:34:56.000Z'); + }); + + test('should handle all variables with timezone and user combined', () => { + const result = replaceSpecialVars({ + text: '{{current_user}} - {{current_date}} - {{current_datetime}} - {{iso_datetime}}', + user: mockUser, + timezone: 'Asia/Tokyo', + }); + expect(result).toBe( + 'Test User - 2024-04-29 (Monday) - 2024-04-29 12:34:56 +09:00 (Monday) - 2024-04-29T16:34:56.000Z', + ); + }); }); describe('parseCompactConvo', () => { diff --git a/packages/data-provider/src/parsers.ts b/packages/data-provider/src/parsers.ts index 3ec4221b62..ee3a500fa0 100644 --- a/packages/data-provider/src/parsers.ts +++ b/packages/data-provider/src/parsers.ts @@ -1,5 +1,10 @@ import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import tz from 'dayjs/plugin/timezone'; import type { ZodIssue } from 'zod'; + +dayjs.extend(utc); +dayjs.extend(tz); import type * as a from './types/assistants'; import type * as s from './schemas'; import type * as t from './types'; @@ -402,13 +407,24 @@ export function findLastSeparatorIndex(text: string, separators = SEPARATORS): n return lastIndex; } -export function replaceSpecialVars({ text, user }: { text: string; user?: t.TUser | null }) { +export function replaceSpecialVars({ + text, + user, + timezone, +}: { + text: string; + user?: t.TUser | null; + timezone?: string; +}) { let result = text; if (!result) { return result; } - const now = dayjs(); + let now = timezone ? dayjs().tz(timezone) : dayjs(); + if (!now.isValid()) { + now = dayjs(); + } const weekdayName = now.format('dddd'); const currentDate = now.format('YYYY-MM-DD'); diff --git a/packages/data-provider/src/schemas.ts b/packages/data-provider/src/schemas.ts index 084f74af86..386ba2d2f2 100644 --- a/packages/data-provider/src/schemas.ts +++ b/packages/data-provider/src/schemas.ts @@ -611,6 +611,7 @@ export const tMessageSchema = z.object({ isCreatedByUser: z.boolean(), error: z.boolean().optional(), clientTimestamp: z.string().optional(), + clientTimezone: z.string().optional(), createdAt: z .string() .optional() @@ -690,6 +691,7 @@ export type TMessage = z.input & { siblingIndex?: number; attachments?: TAttachment[]; clientTimestamp?: string; + clientTimezone?: string; feedback?: TFeedback; };