Render next run time in user timezone in config, chat UIs

- Pass timezone string from ipapi to khoj via clients
  - Pass this data from web, desktop and obsidian clients to server
- Use user tz to render next run time of scheduled task in user tz
This commit is contained in:
Debanjum Singh Solanky 2024-04-27 00:56:49 +05:30
parent 6736551ba3
commit c17dbbeb92
5 changed files with 33 additions and 11 deletions

View file

@ -80,6 +80,7 @@ dependencies = [
"psutil >= 5.8.0", "psutil >= 5.8.0",
"huggingface-hub >= 0.22.2", "huggingface-hub >= 0.22.2",
"apscheduler ~= 3.10.0", "apscheduler ~= 3.10.0",
"pytz ~= 2024.1",
] ]
dynamic = ["version"] dynamic = ["version"]

View file

@ -40,6 +40,7 @@
let region = null; let region = null;
let city = null; let city = null;
let countryName = null; let countryName = null;
let timezone = null;
fetch("https://ipapi.co/json") fetch("https://ipapi.co/json")
.then(response => response.json()) .then(response => response.json())
@ -47,6 +48,7 @@
region = data.region; region = data.region;
city = data.city; city = data.city;
countryName = data.country_name; countryName = data.country_name;
timezone = data.timezone;
}) })
.catch(err => { .catch(err => {
console.log(err); console.log(err);
@ -463,7 +465,7 @@
} }
// Generate backend API URL to execute query // Generate backend API URL to execute query
let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}`; let chatApi = `${hostURL}/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
let newResponseEl = document.createElement("div"); let newResponseEl = document.createElement("div");
newResponseEl.classList.add("chat-message", "khoj"); newResponseEl.classList.add("chat-message", "khoj");

View file

@ -15,6 +15,7 @@ export class KhojChatModal extends Modal {
region: string; region: string;
city: string; city: string;
countryName: string; countryName: string;
timezone: string;
constructor(app: App, setting: KhojSetting) { constructor(app: App, setting: KhojSetting) {
super(app); super(app);
@ -30,6 +31,7 @@ export class KhojChatModal extends Modal {
this.region = data.region; this.region = data.region;
this.city = data.city; this.city = data.city;
this.countryName = data.country_name; this.countryName = data.country_name;
this.timezone = data.timezone;
}) })
.catch(err => { .catch(err => {
console.log(err); console.log(err);
@ -393,7 +395,7 @@ export class KhojChatModal extends Modal {
// Get chat response from Khoj backend // Get chat response from Khoj backend
let encodedQuery = encodeURIComponent(query); let encodedQuery = encodeURIComponent(query);
let chatUrl = `${this.setting.khojUrl}/api/chat?q=${encodedQuery}&n=${this.setting.resultsCount}&client=obsidian&stream=true&region=${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&region=${this.region}&city=${this.city}&country=${this.countryName}&timezone=${this.timezone}`;
let responseElement = this.createKhojResponseDiv(); let responseElement = this.createKhojResponseDiv();
// Temporary status message to indicate that Khoj is thinking // Temporary status message to indicate that Khoj is thinking

View file

@ -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 region = null;
let city = null; let city = null;
let countryName = null; let countryName = null;
let timezone = null;
let waitingForLocation = true; let waitingForLocation = true;
let websocketState = { 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; region = data.region;
city = data.city; city = data.city;
countryName = data.country_name; countryName = data.country_name;
timezone = data.timezone;
}) })
.catch(err => { .catch(err => {
console.log(err); console.log(err);
return; return;
}) })
.finally(() => { .finally(() => {
console.debug("Region:", region, "City:", city, "Country:", countryName); console.debug("Region:", region, "City:", city, "Country:", countryName, "Timezone:", timezone);
waitingForLocation = false; waitingForLocation = false;
setupWebSocket(); 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"); chatInput.classList.remove("option-enabled");
// Generate backend API URL to execute query // Generate backend API URL to execute query
let url = `/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}`; let url = `/api/chat?q=${encodeURIComponent(query)}&n=${resultsCount}&client=web&stream=true&conversation_id=${conversationID}&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}`;
// Call specified Khoj API // Call specified Khoj API
let response = await fetch(url); 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) { if (chatBody.dataset.conversationId) {
webSocketUrl += `?conversation_id=${chatBody.dataset.conversationId}`; webSocketUrl += `?conversation_id=${chatBody.dataset.conversationId}`;
webSocketUrl += (!!region && !!city && !!countryName) ? `&region=${region}&city=${city}&country=${countryName}` : ''; webSocketUrl += (!!region && !!city && !!countryName) && !!timezone ? `&region=${region}&city=${city}&country=${countryName}&timezone=${timezone}` : '';
websocket = new WebSocket(webSocketUrl); websocket = new WebSocket(webSocketUrl);
websocket.onmessage = function(event) { websocket.onmessage = function(event) {

View file

@ -8,6 +8,7 @@ from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
from urllib.parse import unquote from urllib.parse import unquote
import pytz
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket from fastapi import APIRouter, Depends, HTTPException, Request, WebSocket
@ -273,6 +274,7 @@ async def websocket_endpoint(
city: Optional[str] = None, city: Optional[str] = None,
region: Optional[str] = None, region: Optional[str] = None,
country: Optional[str] = None, country: Optional[str] = None,
timezone: Optional[str] = None,
): ):
connection_alive = True connection_alive = True
@ -426,13 +428,19 @@ async def websocket_endpoint(
f"Unable to schedule reminder. Ensure the reminder doesn't already exist." f"Unable to schedule reminder. Ensure the reminder doesn't already exist."
) )
continue 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) 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""" llm_response = f"""
### 🕒 Scheduled Task ### 🕒 Scheduled Task
- Query: **"{unprefixed_inferred_query}"** - Query: **"{unprefixed_inferred_query}"**
- Schedule: `{crontime}` - Schedule: `{crontime}` UTC (+0000)
- Next Run At: **{next_run_time}** UTC. - Next Run At: **{next_run_time}**.
""".strip() """.strip()
await sync_to_async(save_to_conversation_log)( await sync_to_async(save_to_conversation_log)(
@ -608,6 +616,7 @@ async def chat(
city: Optional[str] = None, city: Optional[str] = None,
region: Optional[str] = None, region: Optional[str] = None,
country: Optional[str] = None, country: Optional[str] = None,
timezone: Optional[str] = None,
rate_limiter_per_minute=Depends( rate_limiter_per_minute=Depends(
ApiUserRateLimiter(requests=5, subscribed_requests=60, window=60, slug="chat_minute") ApiUserRateLimiter(requests=5, subscribed_requests=60, window=60, slug="chat_minute")
), ),
@ -691,13 +700,19 @@ async def chat(
status_code=500, 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) 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""" llm_response = f"""
### 🕒 Scheduled Task ### 🕒 Scheduled Task
- Query: **"{unprefixed_inferred_query}"** - Query: **"{unprefixed_inferred_query}"**
- Schedule: `{crontime}` - Schedule: `{crontime}` UTC (+0000)
- Next Run At: **{next_run_time}** UTC.' - Next Run At: **{next_run_time}**.'
""".strip() """.strip()
await sync_to_async(save_to_conversation_log)( await sync_to_async(save_to_conversation_log)(