fix(calendar): correct event timezone handling

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 20:06:09 -04:00
parent 87c55691fb
commit fd05c65018
5 changed files with 33 additions and 6 deletions
+16 -3
View File
@@ -38,17 +38,30 @@ const color = ref("");
const projectId = ref<number | null>(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() {
+1
View File
@@ -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;
+2
View File
@@ -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({
@@ -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
+12 -3
View File
@@ -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}"
]