diff --git a/pyproject.toml b/pyproject.toml index 76928771..fa1db686 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,6 +80,7 @@ dependencies = [ "psutil >= 5.8.0", "huggingface-hub >= 0.22.2", "apscheduler ~= 3.10.0", + "pytz ~= 2024.1", ] dynamic = ["version"] diff --git a/src/interface/desktop/chat.html b/src/interface/desktop/chat.html index a1002ec9..c26fe4b5 100644 --- a/src/interface/desktop/chat.html +++ b/src/interface/desktop/chat.html @@ -40,6 +40,7 @@ let region = null; let city = null; let countryName = null; + let timezone = null; fetch("https://ipapi.co/json") .then(response => response.json()) @@ -47,6 +48,7 @@ region = data.region; city = data.city; countryName = data.country_name; + timezone = data.timezone; }) .catch(err => { console.log(err); @@ -463,7 +465,7 @@ } // Generate backend API URL to execute query - let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}`; + let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`; let newResponseEl = document.createElement("div"); newResponseEl.classList.add("chat-message", "khoj"); diff --git a/src/interface/obsidian/src/chat_modal.ts b/src/interface/obsidian/src/chat_modal.ts index 504ce4db..31b938a1 100644 --- a/src/interface/obsidian/src/chat_modal.ts +++ b/src/interface/obsidian/src/chat_modal.ts @@ -15,6 +15,7 @@ export class KhojChatModal extends Modal { region: string; city: string; countryName: string; + timezone: string; constructor(app: App, setting: KhojSetting) { super(app); @@ -30,6 +31,7 @@ export class KhojChatModal extends Modal { this.region = data.region; this.city = data.city; this.countryName = data.country_name; + this.timezone = data.timezone; }) .catch(err => { console.log(err); @@ -393,7 +395,7 @@ export class KhojChatModal extends Modal { // Get chat response from Khoj backend let encodedQuery = encodeURIComponent(query); - let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&n=${this.setting.resultsCount}&client=obsidian&stream=true®ion=${this.region}&city=${this.city}&country=${this.countryName}`; + let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&n=${this.setting.resultsCount}&client=obsidian&stream=true®ion=${this.region}&city=${this.city}&country=${this.countryName}&timezone=${this.timezone}`; let responseElement = this.createKhojResponseDiv(); // Temporary status message to indicate that Khoj is thinking diff --git a/src/khoj/interface/web/chat.html b/src/khoj/interface/web/chat.html index ef35f35d..5302f311 100644 --- a/src/khoj/interface/web/chat.html +++ b/src/khoj/interface/web/chat.html @@ -58,6 +58,7 @@ To get started, just start typing below. You can also type / to see a list of co let region = null; let city = null; let countryName = null; + let timezone = null; let waitingForLocation = true; let websocketState = { @@ -74,13 +75,14 @@ To get started, just start typing below. You can also type / to see a list of co region = data.region; city = data.city; countryName = data.country_name; + timezone = data.timezone; }) .catch(err => { console.log(err); return; }) .finally(() => { - console.debug("Region:", region, "City:", city, "Country:", countryName); + console.debug("Region:", region, "City:", city, "Country:", countryName, "Timezone:", timezone); waitingForLocation = false; setupWebSocket(); }); @@ -511,7 +513,7 @@ To get started, just start typing below. You can also type / to see a list of co chatInput.classList.remove("option-enabled"); // Generate backend API URL to execute query - let url = `/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}`; + let url = `/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}`; // Call specified Khoj API let response = await fetch(url); @@ -906,7 +908,7 @@ To get started, just start typing below. You can also type / to see a list of co if (chatBody.dataset.conversationId) { webSocketUrl += `?conversation_id=${chatBody.dataset.conversationId}`; - webSocketUrl += (!!region && !!city && !!countryName) ? `®ion=${region}&city=${city}&country=${countryName}` : ''; + webSocketUrl += (!!region && !!city && !!countryName) && !!timezone ? `®ion=${region}&city=${city}&country=${countryName}&timezone=${timezone}` : ''; websocket = new WebSocket(webSocketUrl); websocket.onmessage = function(event) { diff --git a/src/khoj/routers/api_chat.py b/src/khoj/routers/api_chat.py index 1f24b3a2..58cc72bc 100644 --- a/src/khoj/routers/api_chat.py +++ b/src/khoj/routers/api_chat.py @@ -8,6 +8,7 @@ from datetime import datetime from typing import Dict, Optional from urllib.parse import unquote +import pytz from apscheduler.triggers.cron import CronTrigger from asgiref.sync import sync_to_async from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket @@ -273,6 +274,7 @@ async def websocket_endpoint( city: Optional[str] = None, region: Optional[str] = None, country: Optional[str] = None, + timezone: Optional[str] = None, ): connection_alive = True @@ -426,13 +428,19 @@ async def websocket_endpoint( f"Unable to schedule reminder. Ensure the reminder doesn't already exist." ) continue + # Display next run time in user timezone instead of UTC + user_timezone = pytz.timezone(timezone) + next_run_time_utc = job.next_run_time.replace(tzinfo=pytz.utc) + next_run_time_user_tz = next_run_time_utc.astimezone(user_timezone) + next_run_time = next_run_time_user_tz.strftime("%Y-%m-%d %H:%M %Z (%z)") + # Remove /task prefix from inferred_query unprefixed_inferred_query = re.sub(r"^\/task\s*", "", inferred_query) - next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") + # Create the scheduled task response llm_response = f""" ### 🕒 Scheduled Task - Query: **"{unprefixed_inferred_query}"** -- Schedule: `{crontime}` -- Next Run At: **{next_run_time}** UTC. +- Schedule: `{crontime}` UTC (+0000) +- Next Run At: **{next_run_time}**. """.strip() await sync_to_async(save_to_conversation_log)( @@ -608,6 +616,7 @@ async def chat( city: Optional[str] = None, region: Optional[str] = None, country: Optional[str] = None, + timezone: Optional[str] = None, rate_limiter_per_minute=Depends( ApiUserRateLimiter(requests=5, subscribed_requests=60, window=60, slug="chat_minute") ), @@ -691,13 +700,19 @@ async def chat( status_code=500, ) + # Display next run time in user timezone instead of UTC + user_timezone = pytz.timezone(timezone) + next_run_time_utc = job.next_run_time.replace(tzinfo=pytz.utc) + next_run_time_user_tz = next_run_time_utc.astimezone(user_timezone) + next_run_time = next_run_time_user_tz.strftime("%Y-%m-%d %H:%M %Z (%z)") + # Remove /task prefix from inferred_query unprefixed_inferred_query = re.sub(r"^\/task\s*", "", inferred_query) - next_run_time = job.next_run_time.strftime("%Y-%m-%d %H:%M:%S") + # Create the scheduled task response llm_response = f""" ### 🕒 Scheduled Task - Query: **"{unprefixed_inferred_query}"** -- Schedule: `{crontime}` -- Next Run At: **{next_run_time}** UTC.' +- Schedule: `{crontime}` UTC (+0000) +- Next Run At: **{next_run_time}**.' """.strip() await sync_to_async(save_to_conversation_log)(