From fd05c6501896d7ba6ebe5dab095ec0e269f48c0b Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 25 Mar 2026 20:06:09 -0400 Subject: [PATCH] fix(calendar): correct event timezone handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Frontend sends user_timezone (IANA, from Intl.DateTimeFormat) with every message POST; threaded through route → generation_task → build_context - System prompt now tells the LLM the user's timezone so it creates events with the correct UTC offset (e.g. 15:00+01:00 not 15:00Z) - Calendar tool guidance updated to require UTC offset in all event datetimes - EventSlideOver: dateFromIso/timeFromIso now use JS Date to convert stored UTC times to local time for display; toIso includes local timezone offset when saving so the correct UTC time is stored Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/EventSlideOver.vue | 19 ++++++++++++++++--- frontend/src/stores/chat.ts | 1 + src/fabledassistant/routes/chat.py | 2 ++ .../services/generation_task.py | 2 ++ src/fabledassistant/services/llm.py | 15 ++++++++++++--- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/EventSlideOver.vue b/frontend/src/components/EventSlideOver.vue index a04d918..5c5a776 100644 --- a/frontend/src/components/EventSlideOver.vue +++ b/frontend/src/components/EventSlideOver.vue @@ -38,17 +38,30 @@ const color = ref(""); const projectId = ref(null); function dateFromIso(iso: string): string { - return iso.slice(0, 10); + const d = new Date(iso); + if (isNaN(d.getTime())) return iso.slice(0, 10); + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; } function timeFromIso(iso: string): string { if (!iso.includes("T")) return "09:00"; - return iso.slice(11, 16); + const d = new Date(iso); + if (isNaN(d.getTime())) return iso.slice(11, 16); + return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } function toIso(date: string, time: string): string { if (!time) return `${date}T00:00:00`; - return `${date}T${time}:00`; + // Include local timezone offset so the server stores the correct UTC time + const local = new Date(`${date}T${time}:00`); + const off = -local.getTimezoneOffset(); + const sign = off >= 0 ? "+" : "-"; + const h = String(Math.floor(Math.abs(off) / 60)).padStart(2, "0"); + const min = String(Math.abs(off) % 60).padStart(2, "0"); + return `${date}T${time}:00${sign}${h}:${min}`; } function resetForm() { diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts index af19e2c..2b36aa2 100644 --- a/frontend/src/stores/chat.ts +++ b/frontend/src/stores/chat.ts @@ -324,6 +324,7 @@ export const useChatStore = defineStore("chat", () => { think, rag_project_id: ragProjectId ?? undefined, workspace_project_id: workspaceProjectId ?? undefined, + user_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, }, ); assistantMessageId = resp.assistant_message_id; diff --git a/src/fabledassistant/routes/chat.py b/src/fabledassistant/routes/chat.py index 8ba5f89..63cd5e1 100644 --- a/src/fabledassistant/routes/chat.py +++ b/src/fabledassistant/routes/chat.py @@ -148,6 +148,7 @@ async def send_message_route(conv_id: int): think = bool(data.get("think", False)) rag_project_id = data.get("rag_project_id") or None workspace_project_id = data.get("workspace_project_id") or None + user_timezone = data.get("user_timezone") or None effective_rag_project_id = workspace_project_id or rag_project_id # Reject if generation already running for this conversation @@ -184,6 +185,7 @@ async def send_message_route(conv_id: int): think=think, rag_project_id=effective_rag_project_id, workspace_project_id=workspace_project_id, + user_timezone=user_timezone, )) return jsonify({ diff --git a/src/fabledassistant/services/generation_task.py b/src/fabledassistant/services/generation_task.py index a377b56..04fa395 100644 --- a/src/fabledassistant/services/generation_task.py +++ b/src/fabledassistant/services/generation_task.py @@ -151,6 +151,7 @@ async def run_generation( think: bool = False, rag_project_id: int | None = None, workspace_project_id: int | None = None, + user_timezone: str | None = None, ) -> None: """Stream LLM response into buffer with periodic DB flushes.""" MAX_TOOL_ROUNDS = 5 @@ -183,6 +184,7 @@ async def run_generation( excluded_note_ids=excluded_note_ids, rag_project_id=rag_project_id, workspace_project_id=workspace_project_id, + user_timezone=user_timezone, )) messages, context_meta = await context_task diff --git a/src/fabledassistant/services/llm.py b/src/fabledassistant/services/llm.py index 9f24050..3d55624 100644 --- a/src/fabledassistant/services/llm.py +++ b/src/fabledassistant/services/llm.py @@ -452,6 +452,7 @@ async def build_context( excluded_note_ids: list[int] | None = None, rag_project_id: int | None = None, workspace_project_id: int | None = None, + user_timezone: str | None = None, ) -> tuple[list[dict], dict]: """Build messages array for Ollama with system prompt and context. @@ -475,9 +476,16 @@ async def build_context( "Available actions: create_task, create_note, update_note, delete_note, delete_task, get_note, list_notes, list_tasks, search_notes, " "create_event, list_events, search_events, update_event, delete_event, list_calendars." ) - tool_lines.append("For calendar events, use ISO 8601 datetime format (e.g. 2026-09-30T00:00:00).") + tool_lines.append( + "For calendar events, use ISO 8601 datetime format with the user's timezone offset" + + (f" ({user_timezone})" if user_timezone else "") + + ". Always include the UTC offset in datetime strings (e.g. 2026-09-30T14:00:00+01:00)." + ) tool_lines.append("When the user says 'remind me' with a time before an event, use the reminder_minutes parameter.") - tool_lines.append("For relative dates like 'Friday' or 'next week', resolve them to YYYY-MM-DD format.") + tool_lines.append( + "For relative dates like 'Friday' or 'next week', resolve them to YYYY-MM-DD format. " + + (f"Always include the UTC offset when creating events (user's timezone: {user_timezone})." if user_timezone else "For event datetimes, include the UTC offset (e.g. 2026-09-30T14:00:00+01:00).") + ) tool_lines.append("When creating notes, use the `tags` parameter — do not embed #tag text in the note body.") tool_lines.append( "When search_images returns results, embed each image directly in your response by writing " @@ -498,11 +506,12 @@ async def build_context( ) tool_guidance = "\n".join(tool_lines) + tz_line = f" The user's timezone is {user_timezone}." if user_timezone else "" system_parts = [ f"You are a helpful assistant named {assistant_name}, integrated into a note-taking and task-tracking app called Fabled Assistant. " "Help users with their notes, tasks, and general questions. " "When note context is provided, use it to give relevant answers. " - f"Today's date is {today}.\n\n" + f"Today's date is {today}.{tz_line}\n\n" f"{tool_guidance}" ]