diff --git a/documentation/docs/advanced/litellm.md b/documentation/docs/advanced/litellm.md index ad482e50..d7bdea88 100644 --- a/documentation/docs/advanced/litellm.md +++ b/documentation/docs/advanced/litellm.md @@ -34,4 +34,4 @@ Using LiteLLM with Khoj makes it possible to turn any LLM behind an API into you 5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel - Default model: `` - Summarizer model: `` -6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown. +6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown. diff --git a/documentation/docs/advanced/lmstudio.md b/documentation/docs/advanced/lmstudio.md index d8d42664..a7df6d3c 100644 --- a/documentation/docs/advanced/lmstudio.md +++ b/documentation/docs/advanced/lmstudio.md @@ -27,4 +27,4 @@ LM Studio can expose an [OpenAI API compatible server](https://lmstudio.ai/docs/ 5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel - Default model: `` - Summarizer model: `` -6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown. +6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown. diff --git a/documentation/docs/advanced/ollama.md b/documentation/docs/advanced/ollama.md index 4679b1d0..0819a013 100644 --- a/documentation/docs/advanced/ollama.md +++ b/documentation/docs/advanced/ollama.md @@ -31,6 +31,6 @@ Ollama exposes a local [OpenAI API compatible server](https://github.com/ollama/ 5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel - Default model: `` - Summarizer model: `` -6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown. +6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown. That's it! You should now be able to chat with your Ollama model from Khoj. If you want to add additional models running on Ollama, repeat step 6 for each model. diff --git a/documentation/docs/advanced/use-openai-proxy.md b/documentation/docs/advanced/use-openai-proxy.md index 0479b6fe..e67b5800 100644 --- a/documentation/docs/advanced/use-openai-proxy.md +++ b/documentation/docs/advanced/use-openai-proxy.md @@ -34,4 +34,4 @@ For specific integrations, see our [Ollama](/advanced/ollama), [LMStudio](/advan 5. Create a new [Server Chat Setting](http://localhost:42110/server/admin/database/serverchatsettings/add/) on your Khoj admin panel - Default model: `` - Summarizer model: `` -6. Go to [your config](http://localhost:42110/config) and select the model you just created in the chat model dropdown. +6. Go to [your config](http://localhost:42110/settings) and select the model you just created in the chat model dropdown. diff --git a/documentation/docs/clients/desktop.md b/documentation/docs/clients/desktop.md index 6c089424..d32d1ac4 100644 --- a/documentation/docs/clients/desktop.md +++ b/documentation/docs/clients/desktop.md @@ -23,7 +23,7 @@ Khoj will keep these files in sync to provide contextual responses when you sear ## Setup 1. Install the [Khoj Desktop app](https://khoj.dev/downloads) for your OS -2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients) +2. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients) 3. Set your Khoj API Key on the *Settings* page of the Khoj Desktop app 4. [Optional] Add any files, folders you'd like Khoj to be aware of on the *Settings* page and Click *Save* These files and folders will be automatically kept in sync for you diff --git a/documentation/docs/clients/emacs.md b/documentation/docs/clients/emacs.md index 3252c598..6c751927 100644 --- a/documentation/docs/clients/emacs.md +++ b/documentation/docs/clients/emacs.md @@ -30,7 +30,7 @@ sidebar_position: 2 | ![khoj search on emacs](/img/khoj_search_on_emacs.png) | ![khoj chat on emacs](/img/khoj_chat_on_emacs.png) | ## Setup -1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients) +1. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients) 2. Add below snippet to your Emacs config file, usually at `~/.emacs.d/init.el` diff --git a/documentation/docs/clients/obsidian.md b/documentation/docs/clients/obsidian.md index 37a2526e..0372ecbe 100644 --- a/documentation/docs/clients/obsidian.md +++ b/documentation/docs/clients/obsidian.md @@ -23,7 +23,7 @@ sidebar_position: 3 1. Open [Khoj](https://obsidian.md/plugins?id=khoj) from the *Community plugins* tab in Obsidian settings panel 2. Click *Install*, then *Enable* on the Khoj plugin page in Obsidian - 3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/config#clients) + 3. Generate an API key on the [Khoj Web App](https://app.khoj.dev/settings#clients) 4. Set your Khoj API Key in the Khoj plugin settings in Obsidian See the official [Obsidian Plugin Docs](https://help.obsidian.md/Extending+Obsidian/Community+plugins) for more details on installing Obsidian plugins. diff --git a/documentation/docs/clients/whatsapp.md b/documentation/docs/clients/whatsapp.md index 548559da..dccd0cab 100644 --- a/documentation/docs/clients/whatsapp.md +++ b/documentation/docs/clients/whatsapp.md @@ -10,7 +10,7 @@ Text [+1 (848) 800 4242](https://wa.me/18488004242) or scan the QQ code below on Without any desktop clients, you can start chatting with Khoj on WhatsApp. Bear in mind you do need one of the desktop clients in order to share and sync your data with Khoj. The WhatsApp AI bot will work right away for answering generic queries and using Khoj in default mode. -In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/config). +In order to use Khoj on WhatsApp with your own data, you need to setup a Khoj Cloud account and connect your WhatsApp account to it. This is a one time setup and you can do it from the [Khoj Cloud config page](https://app.khoj.dev/settings). If you hit usage limits for the WhatsApp bot, upgrade to [a paid plan](https://khoj.dev/pricing) on Khoj Cloud. diff --git a/documentation/docs/data-sources/github_integration.md b/documentation/docs/data-sources/github_integration.md index 77e5bd1b..71d3ac04 100644 --- a/documentation/docs/data-sources/github_integration.md +++ b/documentation/docs/data-sources/github_integration.md @@ -4,11 +4,11 @@ The Github integration allows you to index as many repositories as you want. It' # Configure your settings -1. Go to [https://app.khoj.dev/config](https://app.khoj.dev/config) and enter in settings for the data sources you want to index. You'll have to specify the file paths. +1. Go to [https://app.khoj.dev/settings](https://app.khoj.dev/settings) and enter in settings for the data sources you want to index. You'll have to specify the file paths. ## Use the Github plugin 1. Generate a [classic PAT (personal access token)](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) from [Github](https://github.com/settings/tokens) with `repo` and `admin:org` scopes at least. -2. Navigate to [https://app.khoj.dev/config/content-source/github](https://app.khoj.dev/config/content-source/github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index. +2. Navigate to [https://app.khoj.dev/settings#github](https://app.khoj.dev/settings#github) to configure your Github settings. Enter in your PAT, along with details for each repository you want to index. 3. Click `Save`. Go back to the settings page and click `Configure`. 4. Go to [https://app.khoj.dev/](https://app.khoj.dev/) and start searching! diff --git a/documentation/docs/data-sources/notion_integration.md b/documentation/docs/data-sources/notion_integration.md index 23fe9f32..4dd90c4a 100644 --- a/documentation/docs/data-sources/notion_integration.md +++ b/documentation/docs/data-sources/notion_integration.md @@ -2,7 +2,7 @@ The Notion integration allows you to search/chat with your Notion workspaces. [Notion](https://notion.so/) is a platform people use for taking notes, especially for collaboration. -Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj. +Go to https://app.khoj.dev/settings to connect your Notion workspace(s) to Khoj. ![notion_integration](https://assets.khoj.dev/notion_integration.gif) @@ -13,7 +13,7 @@ Go to https://app.khoj.dev/config to connect your Notion workspace(s) to Khoj. ![setup_new_integration](https://github.com/khoj-ai/khoj/assets/65192171/b056e057-d4dc-47dc-aad3-57b59a22c68b) 3. Share all the workspaces that you want to integrate with the Khoj integration you just made in the previous step ![enable_workspace](https://github.com/khoj-ai/khoj/assets/65192171/98290303-b5b8-4cb0-b32c-f68c6923a3d0) -4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at http://localhost:42110/config/content-source/notion. Click `Save`. -5. Click `Configure` in http://localhost:42110/config to index your Notion workspace(s). +4. In the first step, you generated an API key. Use the newly generated API Key in your Khoj settings, by default at [http://localhost:42110/settings#notion](http://localhost:42110/settings#notion). Click `Save`. +5. Click `Configure` in http://localhost:42110/settings to index your Notion workspace(s). That's it! You should be ready to start searching and chatting. Make sure you've configured your [chat settings](/get-started/setup#2-configure). diff --git a/documentation/docs/get-started/overview.md b/documentation/docs/get-started/overview.md index 57bc6752..c8864cfc 100644 --- a/documentation/docs/get-started/overview.md +++ b/documentation/docs/get-started/overview.md @@ -1,7 +1,7 @@ --- sidebar_position: 0 slug: / -keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain ai", "ai search engine"] +keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features", "khoj overview", "khoj quickstart", "khoj chat", "khoj search", "khoj cloud", "khoj self-host", "khoj setup", "open source ai", "local llm", "ai copilot", "second brain", "personal ai", "ai search engine"] --- # Overview @@ -9,7 +9,7 @@ keywords: ["khoj", "khoj ai", "khoj docs", "khoj documentation", "khoj features"

Khoj Logo

- An AI copilot for your Second Brain + Your Second Brain

diff --git a/documentation/docusaurus.config.js b/documentation/docusaurus.config.js index f48fd24a..c2228905 100644 --- a/documentation/docusaurus.config.js +++ b/documentation/docusaurus.config.js @@ -9,7 +9,7 @@ import {themes as prismThemes} from 'prism-react-renderer'; /** @type {import('@docusaurus/types').Config} */ const config = { title: 'Khoj AI', - tagline: 'An AI copilot for your Second Brain', + tagline: 'Your Second Brain', staticDirectories: ['assets'], diff --git a/manifest.json b/manifest.json index 2072fbdb..daf7fb89 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "Khoj", "version": "1.17.0", "minAppVersion": "0.15.0", - "description": "An AI copilot for your Second Brain", + "description": "Your Second Brain", "author": "Khoj Inc.", "authorUrl": "https://github.com/khoj-ai", "isDesktopOnly": false diff --git a/pyproject.toml b/pyproject.toml index d41d7977..4b651dad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "khoj" -description = "An AI copilot for your Second Brain" +description = "Your Second Brain" readme = "README.md" license = "AGPL-3.0-or-later" requires-python = ">=3.10" diff --git a/src/interface/desktop/main.js b/src/interface/desktop/main.js index 12d486e9..54cf1fa6 100644 --- a/src/interface/desktop/main.js +++ b/src/interface/desktop/main.js @@ -233,11 +233,15 @@ function pushDataToKhoj (regenerate = false) { // Request indexing files on server. With upto 1000 files in each request for (let i = 0; i < filesDataToPush.length; i += 1000) { + const syncUrl = `${hostURL}/api/content?client=desktop`; const filesDataGroup = filesDataToPush.slice(i, i + 1000); const formData = new FormData(); filesDataGroup.forEach(fileData => { formData.append('files', fileData.blob, fileData.path) }); - let request = axios.post(`${hostURL}/api/v1/index/update?force=${regenerate}&client=desktop`, formData, { headers }); - requests.push(request); + requests.push( + regenerate + ? axios.put(syncUrl, formData, { headers }) + : axios.patch(syncUrl, formData, { headers }) + ); } // Wait for requests batch to finish @@ -253,7 +257,7 @@ function pushDataToKhoj (regenerate = false) { console.error(error); state["completed"] = false; if (error?.response?.status === 429 && (BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')))) { - state["error"] = `Looks like you're out of space to sync your files. Upgrade your plan to unlock more space.`; + state["error"] = `Looks like you're out of space to sync your files. Upgrade your plan to unlock more space.`; const win = BrowserWindow.getAllWindows().find(win => win.webContents.getURL().includes('config')); if (win) win.webContents.send('needsSubscription', true); } else if (error?.code === 'ECONNREFUSED') { diff --git a/src/interface/desktop/package.json b/src/interface/desktop/package.json index c34d338a..f4962a53 100644 --- a/src/interface/desktop/package.json +++ b/src/interface/desktop/package.json @@ -1,8 +1,8 @@ { "name": "Khoj", "version": "1.17.0", - "description": "An AI copilot for your Second Brain", - "author": "Saba Imran, Debanjum Singh Solanky ", + "description": "Your Second Brain", + "author": "Khoj Inc. ", "license": "GPL-3.0-or-later", "homepage": "https://khoj.dev", "repository": "\"https://github.com/khoj-ai/khoj\"", diff --git a/src/interface/desktop/renderer.js b/src/interface/desktop/renderer.js index a571bb3d..78387813 100644 --- a/src/interface/desktop/renderer.js +++ b/src/interface/desktop/renderer.js @@ -182,7 +182,7 @@ window.updateStateAPI.onUpdateState((event, state) => { window.needsSubscriptionAPI.onNeedsSubscription((event, needsSubscription) => { console.log("needs subscription", needsSubscription); if (needsSubscription) { - window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/config"); + window.alert("Looks like you're out of space to sync your files. Upgrade your plan to unlock more space here: https://app.khoj.dev/settings#subscription"); needsSubscriptionElement.style.display = 'block'; } }); diff --git a/src/interface/desktop/search.html b/src/interface/desktop/search.html index 1f2133fe..792470a6 100644 --- a/src/interface/desktop/search.html +++ b/src/interface/desktop/search.html @@ -212,12 +212,12 @@ const headers = { 'Authorization': `Bearer ${khojToken}` }; // Populate type dropdown field with enabled content types only - fetch(`${hostURL}/api/config/types`, { headers }) + fetch(`${hostURL}/api/content/types`, { headers }) .then(response => response.json()) .then(enabled_types => { // Show warning if no content types are enabled if (enabled_types.detail) { - document.getElementById("results").innerHTML = "
To use Khoj search, setup your content plugins on the Khoj settings page.
"; + document.getElementById("results").innerHTML = "
To use Khoj search, setup your content plugins on the Khoj settings page.
"; document.getElementById("query").setAttribute("disabled", "disabled"); document.getElementById("query").setAttribute("placeholder", "Configure Khoj to enable search"); return []; diff --git a/src/interface/desktop/config.html b/src/interface/desktop/settings.html similarity index 100% rename from src/interface/desktop/config.html rename to src/interface/desktop/settings.html diff --git a/src/interface/desktop/utils.js b/src/interface/desktop/utils.js index af0234ea..c8ed7796 100644 --- a/src/interface/desktop/utils.js +++ b/src/interface/desktop/utils.js @@ -85,7 +85,7 @@ async function populateHeaderPane() { ` : ''} diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el index cc050eed..e59afbc8 100644 --- a/src/interface/emacs/khoj.el +++ b/src/interface/emacs/khoj.el @@ -4,8 +4,8 @@ ;; Author: Debanjum Singh Solanky ;; Saba Imran -;; Description: An AI copilot for your Second Brain -;; Keywords: search, chat, org-mode, outlines, markdown, pdf, image +;; Description: Your Second Brain +;; Keywords: search, chat, ai, org-mode, outlines, markdown, pdf, image ;; Version: 1.17.0 ;; Package-Requires: ((emacs "27.1") (transient "0.3.0") (dash "2.19.1")) ;; URL: https://github.com/khoj-ai/khoj/tree/master/src/interface/emacs @@ -99,7 +99,7 @@ :type 'boolean) (defcustom khoj-api-key nil - "API Key to your Khoj. Default at https://app.khoj.dev/config#clients." + "API Key to your Khoj. Default at https://app.khoj.dev/settings#clients." :group 'khoj :type 'string) @@ -424,12 +424,12 @@ Auto invokes setup steps on calling main entrypoint." "Send multi-part form `BODY' of `CONTENT-TYPE' in request to khoj server. Append 'TYPE-QUERY' as query parameter in request url. Specify `BOUNDARY' used to separate files in request header." - (let ((url-request-method "POST") + (let ((url-request-method ((if force) "PUT" "PATCH")) (url-request-data body) (url-request-extra-headers `(("content-type" . ,(format "multipart/form-data; boundary=%s" boundary)) ("Authorization" . ,(format "Bearer %s" khoj-api-key))))) (with-current-buffer - (url-retrieve (format "%s/api/v1/index/update?%s&force=%s&client=emacs" khoj-server-url type-query (or force "false")) + (url-retrieve (format "%s/api/content?%s&client=emacs" khoj-server-url type-query) ;; render response from indexing API endpoint on server (lambda (status) (if (not (plist-get status :error)) @@ -697,7 +697,7 @@ Optionally apply CALLBACK with JSON parsed response and CBARGS." (defun khoj--get-enabled-content-types () "Get content types enabled for search from API." - (khoj--call-api "/api/config/types" "GET" nil `(lambda (item) (mapcar #'intern item)))) + (khoj--call-api "/api/content/types" "GET" nil `(lambda (item) (mapcar #'intern item)))) (defun khoj--query-search-api-and-render-results (query content-type buffer-name &optional rerank is-find-similar) "Query Khoj Search API with QUERY, CONTENT-TYPE and RERANK as query params. diff --git a/src/interface/obsidian/manifest.json b/src/interface/obsidian/manifest.json index 2072fbdb..daf7fb89 100644 --- a/src/interface/obsidian/manifest.json +++ b/src/interface/obsidian/manifest.json @@ -3,7 +3,7 @@ "name": "Khoj", "version": "1.17.0", "minAppVersion": "0.15.0", - "description": "An AI copilot for your Second Brain", + "description": "Your Second Brain", "author": "Khoj Inc.", "authorUrl": "https://github.com/khoj-ai", "isDesktopOnly": false diff --git a/src/interface/obsidian/package.json b/src/interface/obsidian/package.json index b48caf59..fe5b1a35 100644 --- a/src/interface/obsidian/package.json +++ b/src/interface/obsidian/package.json @@ -1,7 +1,7 @@ { "name": "Khoj", "version": "1.17.0", - "description": "An AI copilot for your Second Brain", + "description": "Your Second Brain", "author": "Debanjum Singh Solanky, Saba Imran ", "license": "GPL-3.0-or-later", "main": "src/main.js", @@ -14,7 +14,8 @@ "search", "chat", "AI", - "assistant" + "assistant", + "second brain" ], "devDependencies": { "@types/dompurify": "^3.0.5", diff --git a/src/interface/obsidian/src/utils.ts b/src/interface/obsidian/src/utils.ts index 14825543..0f4d10ed 100644 --- a/src/interface/obsidian/src/utils.ts +++ b/src/interface/obsidian/src/utils.ts @@ -92,10 +92,11 @@ export async function updateContentIndex(vault: Vault, setting: KhojSetting, las for (let i = 0; i < fileData.length; i += 1000) { const filesGroup = fileData.slice(i, i + 1000); const formData = new FormData(); + const method = regenerate ? "PUT" : "PATCH"; filesGroup.forEach(fileItem => { formData.append('files', fileItem.blob, fileItem.path) }); // Call Khoj backend to update index with all markdown, pdf files - const response = await fetch(`${setting.khojUrl}/api/v1/index/update?force=${regenerate}&client=obsidian`, { - method: 'POST', + const response = await fetch(`${setting.khojUrl}/api/content?client=obsidian`, { + method: method, headers: { 'Authorization': `Bearer ${setting.khojApiKey}`, }, @@ -204,12 +205,12 @@ export function getBackendStatusMessage( ): string { // Welcome message with default settings. Khoj cloud always expects an API key. if (!khojApiKey && khojUrl === 'https://app.khoj.dev') - return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/config#clients and set it in the Khoj plugin settings on Obsidian`; + return `🌈 Welcome to Khoj! Get your API key from ${khojUrl}/settings#clients and set it in the Khoj plugin settings on Obsidian`; if (!connectedToServer) return `❗️Could not connect to Khoj at ${khojUrl}. Ensure your can access it`; else if (!userEmail) - return `✅ Connected to Khoj. ❗️Get a valid API key from ${khojUrl}/config#clients to log in`; + return `✅ Connected to Khoj. ❗️Get a valid API key from ${khojUrl}/settings#clients to log in`; else if (userEmail === 'default@example.com') // Logged in as default user in anonymous mode return `✅ Signed in to Khoj`; diff --git a/src/interface/web/.eslintrc.json b/src/interface/web/.eslintrc.json index bffb357a..1a2d5517 100644 --- a/src/interface/web/.eslintrc.json +++ b/src/interface/web/.eslintrc.json @@ -1,3 +1,11 @@ { - "extends": "next/core-web-vitals" + "extends": [ + "next", + "next/core-web-vitals", + "plugin:prettier/recommended" + ], + "plugins": ["prettier"], + "rules": { + "prettier/prettier": "warn" + } } diff --git a/src/interface/web/.husky/pre-commit b/src/interface/web/.husky/pre-commit new file mode 100755 index 00000000..053576eb --- /dev/null +++ b/src/interface/web/.husky/pre-commit @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +. "$(dirname "$0")/_/husky.sh" + +yarn run lint-staged +yarn test diff --git a/src/interface/web/app/agents/agents.module.css b/src/interface/web/app/agents/agents.module.css index 671f8ecc..8cc0cda5 100644 --- a/src/interface/web/app/agents/agents.module.css +++ b/src/interface/web/app/agents/agents.module.css @@ -6,7 +6,7 @@ div.titleBar { .agentPersonality p { white-space: inherit; overflow: hidden; - height: 78px; + height: 77px; line-height: 1.5; } @@ -16,27 +16,15 @@ div.agentPersonality { overflow: hidden; } -div.agentInfo { - font-size: medium; +div.pageLayout { + max-width: 60vw; + margin: auto; + margin-bottom: 2rem; } -div.agentInfo a, -div.agentInfo h2 { - margin: 0; -} - -div.agent img { - border-radius: 50%; - object-fit: cover; -} - -div.agent a { - text-decoration: none; -} - -div#agentsHeader { - display: grid; - grid-template-columns: auto; +div.sidePanel { + position: fixed; + height: 100%; } button.infoButton { @@ -47,163 +35,33 @@ button.infoButton { font-size: medium; } -div#agentsHeader a, -div.agentInfo button { - font-size: 24px; - font-weight: bold; - padding: 4px; - border: none; - border-radius: 8px; - font: inherit; - cursor: pointer; - transition: background-color 0.3s; -} - -div#agentsHeader a:hover, -div.agentInfo button:hover { - box-shadow: 0 0 10px var(--primary-hover); -} - -div.agent { - display: grid; - grid-template-columns: auto 1fr auto; - gap: 4px; - align-items: center; - padding: 20px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); - border-radius: 8px; - background: linear-gradient(18.48deg,rgba(252, 213, 87, 0.25) 2.76%,rgba(197, 0, 0, 0) 17.23%),linear-gradient(200.6deg,rgba(244, 229, 68, 0.25) 4.13%,rgba(230, 26, 26, 0) 20.54%); -} - -div.agentModal { - background: linear-gradient(18.48deg,rgba(252, 213, 87, 0.25) 2.76%,rgba(197, 0, 0, 0) 17.23%),linear-gradient(200.6deg,rgba(244, 229, 68, 0.25) 4.13%,rgba(230, 26, 26, 0) 20.54%); -} - -div.agentModalContent button { - width: 100%; - margin: 10px 0; - padding: 8px; -} - -div.agentModalHeader { - display: grid; - grid-template-columns: 1fr auto; -} - -div.agentAvatar { - display: flex; - align-items: center; - gap: 8px; -} - -div.agentModalContent p { - white-space: break-spaces; - line-height: 1.5; -} - -div.agentInfo { - text-align: left; -} div.agentList { display: grid; gap: 20px; - padding: 20px; + padding-top: 30px; margin-right: auto; grid-auto-flow: row; grid-template-columns: 1fr 1fr; margin-left: auto; } -svg.newConvoButton { - width: 20px; - margin-left: 5px; -} -div.agentModalContainer { - position: fixed; /* Changed from absolute to fixed */ - top: 0; - left: 0; - width: 100%; - height: 100%; /* This ensures it covers the viewport height */ - display: flex; - justify-content: center; - align-items: center; - background-color: rgba(1,1,1,0.5); - z-index: 1000; /* Ensure it's above other content */ - overflow-y: auto; /* Allows scrolling within the modal if needed */ -} - -div.agentModal { - position: relative; - width: 50%; - margin: auto; - padding: 20px; - background-color: white; - border-radius: 8px; - box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); -} - -div.agentModalActions { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; -} - -div.agentModalActions button { - padding: 8px; - border: none; - border-radius: 8px; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s; -} - -div.agentModalActions button:hover { - box-shadow: 0 0 10px var(hsla(--background)); -} - -@media only screen and (max-width: 700px) { +@media only screen and (max-width: 768px) { div.agentList { - width: 90%; + width: 100%; padding: 0; margin-right: auto; margin-left: auto; grid-template-columns: 1fr; } - div.agentModal { - width: 90%; + div.pageLayout { + max-width: 90vw; } -} -.loader { - width: 48px; - height: 48px; - border-radius: 50%; - display: inline-block; - border-top: 4px solid var(--primary-color); - border-right: 4px solid transparent; - box-sizing: border-box; - animation: rotation 1s linear infinite; -} -.loader::after { - content: ''; - box-sizing: border-box; - position: absolute; - left: 0; - top: 0; - width: 48px; - height: 48px; - border-radius: 50%; - border-bottom: 4px solid transparent; - animation: rotation 0.5s linear infinite reverse; -} -@keyframes rotation { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); + div.sidePanel { + position: relative; + height: 100%; } } diff --git a/src/interface/web/app/agents/agentsLayout.module.css b/src/interface/web/app/agents/agentsLayout.module.css deleted file mode 100644 index fd80922e..00000000 --- a/src/interface/web/app/agents/agentsLayout.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.agentsLayout { - max-width: 70vw; - margin: auto; -} - -@media screen and (max-width: 700px) { - .agentsLayout { - max-width: 90vw; - } -} diff --git a/src/interface/web/app/agents/layout.tsx b/src/interface/web/app/agents/layout.tsx index 7e8e3d59..728ad4bb 100644 --- a/src/interface/web/app/agents/layout.tsx +++ b/src/interface/web/app/agents/layout.tsx @@ -1,13 +1,14 @@ - import type { Metadata } from "next"; -import NavMenu from '../components/navMenu/navMenu'; -import styles from './agentsLayout.module.css'; +import { Noto_Sans } from "next/font/google"; +import "../globals.css"; + +const inter = Noto_Sans({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Khoj AI - Agents", - description: "Use Agents with Khoj AI for deeper, more personalized queries.", + description: "Find a specialized agent that can help you address more specific needs.", icons: { - icon: '/static/favicon.ico', + icon: "/static/favicon.ico", }, }; @@ -17,9 +18,20 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( -
- - {children} -
+ + + {children} + ); } diff --git a/src/interface/web/app/agents/page.tsx b/src/interface/web/app/agents/page.tsx index 4ea1c7fe..c9e8a1bd 100644 --- a/src/interface/web/app/agents/page.tsx +++ b/src/interface/web/app/agents/page.tsx @@ -1,26 +1,61 @@ -'use client' +"use client"; -import styles from './agents.module.css'; +import styles from "./agents.module.css"; -import Image from 'next/image'; -import Link from 'next/link'; -import useSWR from 'swr'; +import Image from "next/image"; +import useSWR from "swr"; -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; -import { useAuthenticatedData, UserProfile } from '../common/auth'; -import { Button } from '@/components/ui/button'; +import { useAuthenticatedData, UserProfile } from "../common/auth"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { PaperPlaneTilt, Lightning, Plus } from "@phosphor-icons/react"; + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/components/ui/drawer"; +import LoginPrompt from "../components/loginPrompt/loginPrompt"; +import { InlineLoading } from "../components/loading/loading"; +import SidePanel from "../components/sidePanel/chatHistorySidePanel"; +import NavMenu from "../components/navMenu/navMenu"; +import { getIconFromIconName } from "../common/iconUtils"; +import { convertColorToTextClass } from "../common/colorUtils"; +import { Alert, AlertDescription } from "@/components/ui/alert"; export interface AgentData { slug: string; avatar: string; name: string; - personality: string; + persona: string; + color: string; + icon: string; } async function openChat(slug: string, userData: UserProfile | null) { - const unauthenticatedRedirectUrl = `/login?next=/agents?agent=${slug}`; if (!userData) { window.location.href = unauthenticatedRedirectUrl; @@ -28,160 +63,226 @@ async function openChat(slug: string, userData: UserProfile | null) { } const response = await fetch(`/api/chat/sessions?agent_slug=${slug}`, { method: "POST" }); + const data = await response.json(); if (response.status == 200) { - window.location.href = `/chat`; - } else if(response.status == 403 || response.status == 401) { + window.location.href = `/chat?conversationId=${data.conversation_id}`; + } else if (response.status == 403 || response.status == 401) { window.location.href = unauthenticatedRedirectUrl; } else { alert("Failed to start chat session"); } } -const agentsFetcher = () => window.fetch('/api/agents').then(res => res.json()).catch(err => console.log(err)); - -interface AgentModalProps { - data: AgentData; - setShowModal: (show: boolean) => void; - userData: UserProfile | null; -} +const agentsFetcher = () => + window + .fetch("/api/agents") + .then((res) => res.json()) + .catch((err) => console.log(err)); interface AgentCardProps { data: AgentData; userProfile: UserProfile | null; -} - -function AgentModal(props: AgentModalProps) { - const [copiedToClipboard, setCopiedToClipboard] = useState(false); - - useEffect(() => { - if (copiedToClipboard) { - setTimeout(() => setCopiedToClipboard(false), 3000); - } - }, [copiedToClipboard]); - - return ( -
-
-
-
-
- {props.data.name} -

{props.data.name}

-
-
- - -
-
-

{props.data.personality}

-
- -
-
-
-
- ); + isMobileWidth: boolean; } function AgentCard(props: AgentCardProps) { const searchParams = new URLSearchParams(window.location.search); - const agentSlug = searchParams.get('agent'); + const agentSlug = searchParams.get("agent"); const [showModal, setShowModal] = useState(agentSlug === props.data.slug); + const [showLoginPrompt, setShowLoginPrompt] = useState(false); const userData = props.userProfile; if (showModal) { - window.history.pushState({}, `Khoj AI - Agent ${props.data.slug}`, `/agents?agent=${props.data.slug}`); + window.history.pushState( + {}, + `Khoj AI - Agent ${props.data.slug}`, + `/agents?agent=${props.data.slug}`, + ); } + const stylingString = convertColorToTextClass(props.data.color); + return ( -
- { - showModal && - } - -
- {props.data.name} + + {showLoginPrompt && ( + + )} + + + {!props.isMobileWidth ? ( + { + setShowModal(!showModal); + window.history.pushState({}, `Khoj AI - Agents`, `/agents`); + }} + > + +
+ {getIconFromIconName(props.data.icon, props.data.color) || ( + {props.data.name} + )} + {props.data.name} +
+
+
+ {props.userProfile ? ( + + ) : ( + + )} +
+ + +
+ {getIconFromIconName(props.data.icon, props.data.color) || ( + {props.data.name} + )} +

{props.data.name}

+
+
+
+ {props.data.persona} +
+ + + +
+
+ ) : ( + { + setShowModal(open); + window.history.pushState({}, `Khoj AI - Agents`, `/agents`); + }} + > + +
+ {getIconFromIconName(props.data.icon, props.data.color) || ( + {props.data.name} + )} + {props.data.name} +
+
+
+ {props.userProfile ? ( + + ) : ( + + )} +
+ + + {props.data.name} + Full Prompt + + {props.data.persona} + + Done + + +
+ )} +
+
+ +
+
- -
- -
-
- -
-
- -
-
+ + ); } export default function Agents() { - const { data, error } = useSWR('agents', agentsFetcher, { revalidateOnFocus: false }); - const userData = useAuthenticatedData(); + const { data, error } = useSWR("agents", agentsFetcher, { + revalidateOnFocus: false, + }); + const authenticatedData = useAuthenticatedData(); + const [isMobileWidth, setIsMobileWidth] = useState(false); + const [showLoginPrompt, setShowLoginPrompt] = useState(false); + + useEffect(() => { + if (typeof window !== "undefined") { + setIsMobileWidth(window.innerWidth < 768); + } + + window.addEventListener("resize", () => { + setIsMobileWidth(window.innerWidth < 768); + }); + }, []); if (error) { return (
-
- Talk to a Specialized Agent -
-
- Error loading agents -
+
Agents
+
Error loading agents
); } @@ -189,25 +290,66 @@ export default function Agents() { if (!data) { return (
-
- Talk to a Specialized Agent -
- Loading agents... + booting up your agents
); } return ( -
-
- Talk to a Specialized Agent -
-
- {data.map(agent => ( - - ))} +
+
+
+ +
+
+
+

Agents

+
+ + + +
+ +

Create Agent

+
+
+ +

Coming Soon!

+
+
+
+
+
+ {showLoginPrompt && ( + + )} + + + + How it works Use any of these + specialized personas to tune your conversation to your needs. + + +
+ {data.map((agent) => ( + + ))} +
+
); diff --git a/src/interface/web/app/automations/automations.module.css b/src/interface/web/app/automations/automations.module.css new file mode 100644 index 00000000..f0c35244 --- /dev/null +++ b/src/interface/web/app/automations/automations.module.css @@ -0,0 +1,36 @@ +div.automationsLayout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +div.automationCard { + display: grid; + grid-template-rows: auto 1fr auto; +} + +div.pageLayout { + max-width: 60vw; + margin: auto; + margin-bottom: 2rem; +} + +div.sidePanel { + position: fixed; + height: 100%; +} + +@media screen and (max-width: 768px) { + div.automationsLayout { + grid-template-columns: 1fr; + } + + div.pageLayout { + max-width: 90vw; + } + + div.sidePanel { + position: relative; + height: 100%; + } +} diff --git a/src/interface/web/app/automations/layout.tsx b/src/interface/web/app/automations/layout.tsx new file mode 100644 index 00000000..285b200f --- /dev/null +++ b/src/interface/web/app/automations/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Toaster } from "@/components/ui/toaster"; + +import "../globals.css"; + +export const metadata: Metadata = { + title: "Khoj AI - Automations", + description: "Use Autoomations with Khoj to simplify the process of running repetitive tasks.", + icons: { + icon: "/static/favicon.ico", + }, +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ {children} + +
+ ); +} diff --git a/src/interface/web/app/automations/page.tsx b/src/interface/web/app/automations/page.tsx new file mode 100644 index 00000000..7db98e77 --- /dev/null +++ b/src/interface/web/app/automations/page.tsx @@ -0,0 +1,1185 @@ +"use client"; + +import useSWR from "swr"; +import { InlineLoading } from "../components/loading/loading"; +import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button, buttonVariants } from "@/components/ui/button"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; + +interface AutomationsData { + id: number; + subject: string; + query_to_run: string; + scheduling_request: string; + schedule: string; + crontime: string; + next: string; +} + +import cronstrue from "cronstrue"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { UseFormReturn, useForm } from "react-hook-form"; +import { z } from "zod"; +import { Suspense, useEffect, useState } from "react"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { DialogTitle } from "@radix-ui/react-dialog"; +import { Textarea } from "@/components/ui/textarea"; +import { LocationData, useIPLocationData } from "../common/utils"; + +import styles from "./automations.module.css"; +import ShareLink from "../components/shareLink/shareLink"; +import { useSearchParams } from "next/navigation"; +import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover"; +import { + CalendarCheck, + CalendarDot, + CalendarDots, + Clock, + ClockAfternoon, + ClockCounterClockwise, + DotsThreeVertical, + Envelope, + Info, + Lightning, + MapPinSimple, + Pencil, + Play, + Plus, + Trash, +} from "@phosphor-icons/react"; +import { useAuthenticatedData, UserProfile } from "../common/auth"; +import LoginPrompt from "../components/loginPrompt/loginPrompt"; +import { useToast } from "@/components/ui/use-toast"; +import { ToastAction } from "@/components/ui/toast"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import SidePanel from "../components/sidePanel/chatHistorySidePanel"; +import { Drawer, DrawerContent, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer"; + +const automationsFetcher = () => + window + .fetch("/api/automations") + .then((res) => res.json()) + .catch((err) => console.log(err)); + +// Standard cron format: minute hour dayOfMonth month dayOfWeek + +function getEveryBlahFromCron(cron: string) { + const cronParts = cron.split(" "); + const dayOfMonth = cronParts[2]; + const dayOfWeek = cronParts[4]; + + // If both dayOfMonth and dayOfWeek are '*', it runs every day + if (dayOfMonth === "*" && dayOfWeek === "*") { + return "Day"; + } + // If dayOfWeek is not '*', it suggests a specific day of the week, implying a weekly schedule + else if (dayOfWeek !== "*") { + return "Week"; + } + // If dayOfMonth is not '*', it suggests a specific day of the month, implying a monthly schedule + else if (dayOfMonth !== "*") { + return "Month"; + } + // Default to 'Day' if none of the above conditions are met + else { + return "Day"; + } +} + +function getDayOfWeekFromCron(cron: string) { + const cronParts = cron.split(" "); + if (cronParts[3] === "*" && cronParts[4] !== "*") { + return Number(cronParts[4]); + } + + return undefined; +} + +function getTimeRecurrenceFromCron(cron: string) { + const cronParts = cron.split(" "); + const hour = cronParts[1]; + const minute = cronParts[0]; + const period = Number(hour) >= 12 ? "PM" : "AM"; + + let friendlyHour = Number(hour) > 12 ? Number(hour) - 12 : hour; + if (friendlyHour === "00") { + friendlyHour = "12"; + } + + let friendlyMinute = minute; + if (Number(friendlyMinute) < 10 && friendlyMinute !== "00") { + friendlyMinute = `0${friendlyMinute}`; + } + return `${friendlyHour}:${friendlyMinute} ${period}`; +} + +function getDayOfMonthFromCron(cron: string) { + const cronParts = cron.split(" "); + + return String(cronParts[2]); +} + +function cronToHumanReadableString(cron: string) { + return cronstrue.toString(cron); +} + +const frequencies = ["Day", "Week", "Month"]; + +const daysOfMonth = Array.from({ length: 31 }, (_, i) => String(i + 1)); + +const weekDays = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; + +const timeOptions: string[] = []; + +const timePeriods = ["AM", "PM"]; + +// Populate the time selector with options for each hour of the day +for (var i = 0; i < timePeriods.length; i++) { + for (var hour = 0; hour < 12; hour++) { + for (var minute = 0; minute < 60; minute += 15) { + // Ensure all minutes are two digits + const paddedMinute = String(minute).padStart(2, "0"); + const friendlyHour = hour === 0 ? 12 : hour; + timeOptions.push(`${friendlyHour}:${paddedMinute} ${timePeriods[i]}`); + } + } +} + +const timestamp = Date.now(); + +const suggestedAutomationsMetadata: AutomationsData[] = [ + { + subject: "Weekly Newsletter", + query_to_run: + "Compile a message including: 1. A recap of news from last week 2. An at-home workout I can do before work 3. A quote to inspire me for the week ahead", + schedule: "9AM every Monday", + next: "Next run at 9AM on Monday", + crontime: "0 9 * * 1", + id: timestamp, + scheduling_request: "", + }, + { + subject: "Daily Bedtime Story", + query_to_run: + "Compose a bedtime story that a five-year-old might enjoy. It should not exceed five paragraphs. Appeal to the imagination, but weave in learnings.", + schedule: "9PM every night", + next: "Next run at 9PM today", + crontime: "0 21 * * *", + id: timestamp + 1, + scheduling_request: "", + }, + { + subject: "Front Page of Hacker News", + query_to_run: + "Summarize the top 5 posts from https://news.ycombinator.com/best and share them with me, including links", + schedule: "9PM on every Wednesday", + next: "Next run at 9PM on Wednesday", + crontime: "0 21 * * 3", + id: timestamp + 2, + scheduling_request: "", + }, + { + subject: "Market Summary", + query_to_run: + "Get the market summary for today and share it with me. Focus on tech stocks and the S&P 500.", + schedule: "9AM on every weekday", + next: "Next run at 9AM on Monday", + crontime: "0 9 * * *", + id: timestamp + 3, + scheduling_request: "", + }, +]; + +function createShareLink(automation: AutomationsData) { + const encodedSubject = encodeURIComponent(automation.subject); + const encodedQuery = encodeURIComponent(automation.query_to_run); + const encodedCrontime = encodeURIComponent(automation.crontime); + + const shareLink = `${window.location.origin}/automations?subject=${encodedSubject}&query=${encodedQuery}&crontime=${encodedCrontime}`; + + return shareLink; +} + +function deleteAutomation(automationId: string, setIsDeleted: (isDeleted: boolean) => void) { + fetch(`/api/automation?automation_id=${automationId}`, { method: "DELETE" }) + .then((response) => response.json()) + .then((data) => { + setIsDeleted(true); + }); +} + +function sendAPreview(automationId: string, setToastMessage: (toastMessage: string) => void) { + fetch(`/api/trigger/automation?automation_id=${automationId}`, { method: "POST" }) + .then((response) => { + if (!response.ok) { + throw new Error("Network response was not ok"); + } + return response; + }) + .then((automations) => { + setToastMessage("Automation triggered. Check your inbox in a few minutes!"); + }) + .catch((error) => { + setToastMessage("Sorry, something went wrong. Try again later."); + }); +} + +interface AutomationsCardProps { + automation: AutomationsData; + isMobileWidth: boolean; + locationData?: LocationData | null; + suggestedCard?: boolean; + setNewAutomationData?: (data: AutomationsData) => void; + isLoggedIn: boolean; + setShowLoginPrompt: (showLoginPrompt: boolean) => void; + authenticatedData: UserProfile | null; +} + +function AutomationsCard(props: AutomationsCardProps) { + const [isEditing, setIsEditing] = useState(false); + const [updatedAutomationData, setUpdatedAutomationData] = useState( + null, + ); + const [isDeleted, setIsDeleted] = useState(false); + const [toastMessage, setToastMessage] = useState(""); + const { toast } = useToast(); + + const automation = props.automation; + + const [timeRecurrence, setTimeRecurrence] = useState(""); + + const [intervalString, setIntervalString] = useState(""); + + useEffect(() => { + // The updated automation data, if present, takes priority over the original automation data + const automationData = updatedAutomationData || automation; + setTimeRecurrence(getTimeRecurrenceFromCron(automationData.crontime)); + const frequency = getEveryBlahFromCron(automationData.crontime); + + if (frequency === "Day") { + setIntervalString("Daily"); + } else if (frequency === "Week") { + const dayOfWeek = getDayOfWeekFromCron(automationData.crontime); + if (dayOfWeek === undefined) { + setIntervalString("Weekly"); + } else { + setIntervalString(`${weekDays[dayOfWeek]}`); + } + } else if (frequency === "Month") { + const dayOfMonth = getDayOfMonthFromCron(automationData.crontime); + setIntervalString(`Monthly on the ${dayOfMonth}`); + } + }, [updatedAutomationData, automation]); + + useEffect(() => { + const toastTitle = `Automation: ${updatedAutomationData?.subject || automation.subject}`; + if (toastMessage) { + toast({ + title: toastTitle, + description: toastMessage, + action: Ok, + }); + setToastMessage(""); + } + }, [toastMessage, updatedAutomationData, automation, toast]); + + if (isDeleted) { + return null; + } + + return ( + + + + {updatedAutomationData?.subject || automation.subject} + + + + + + {!props.suggestedCard && props.locationData && ( + + )} + { + navigator.clipboard.writeText(createShareLink(automation)); + }} + /> + {!props.suggestedCard && ( + + )} + + + + + + + {updatedAutomationData?.query_to_run || automation.query_to_run} + + +
+
+ +
+ {timeRecurrence} +
+
+
+ +
+ {intervalString} +
+
+
+ {props.suggestedCard && props.setNewAutomationData && ( + + )} +
+
+ ); +} + +interface SharedAutomationCardProps { + locationData?: LocationData | null; + setNewAutomationData: (data: AutomationsData) => void; + isLoggedIn: boolean; + setShowLoginPrompt: (showLoginPrompt: boolean) => void; + authenticatedData: UserProfile | null; + isMobileWidth: boolean; +} + +function SharedAutomationCard(props: SharedAutomationCardProps) { + const searchParams = useSearchParams(); + const [isCreating, setIsCreating] = useState(true); + + const subject = searchParams.get("subject"); + const query = searchParams.get("query"); + const crontime = searchParams.get("crontime"); + + if (!subject || !query || !crontime) { + return null; + } + + const automation: AutomationsData = { + id: 0, + subject: decodeURIComponent(subject), + query_to_run: decodeURIComponent(query), + scheduling_request: "", + schedule: cronToHumanReadableString(decodeURIComponent(crontime)), + crontime: decodeURIComponent(crontime), + next: "", + }; + + return isCreating ? ( + + ) : null; +} + +const EditAutomationSchema = z.object({ + subject: z.optional(z.string()), + everyBlah: z.string({ required_error: "Every is required" }), + dayOfWeek: z.optional(z.number()), + dayOfMonth: z.optional(z.string()), + timeRecurrence: z.string({ required_error: "Time Recurrence is required" }), + queryToRun: z.string({ required_error: "Query to Run is required" }), +}); + +interface EditCardProps { + automation?: AutomationsData; + setIsEditing: (completed: boolean) => void; + setUpdatedAutomationData: (data: AutomationsData) => void; + locationData?: LocationData | null; + createNew?: boolean; + isLoggedIn: boolean; + setShowLoginPrompt: (showLoginPrompt: boolean) => void; + authenticatedData: UserProfile | null; +} + +function EditCard(props: EditCardProps) { + const automation = props.automation; + + const form = useForm>({ + resolver: zodResolver(EditAutomationSchema), + defaultValues: { + subject: automation?.subject, + everyBlah: automation?.crontime ? getEveryBlahFromCron(automation.crontime) : "Day", + dayOfWeek: automation?.crontime ? getDayOfWeekFromCron(automation.crontime) : undefined, + timeRecurrence: automation?.crontime + ? getTimeRecurrenceFromCron(automation.crontime) + : "12:00 PM", + dayOfMonth: automation?.crontime ? getDayOfMonthFromCron(automation.crontime) : "1", + queryToRun: automation?.query_to_run, + }, + }); + + const onSubmit = (values: z.infer) => { + const cronFrequency = convertFrequencyToCron( + values.everyBlah, + values.timeRecurrence, + values.dayOfWeek, + values.dayOfMonth, + ); + + let updateQueryUrl = `/api/automation?`; + + updateQueryUrl += `q=${values.queryToRun}`; + + if (automation?.id && !props.createNew) { + updateQueryUrl += `&automation_id=${automation.id}`; + } + + if (values.subject) { + updateQueryUrl += `&subject=${values.subject}`; + } + + updateQueryUrl += `&crontime=${cronFrequency}`; + + if (props.locationData) { + updateQueryUrl += `&city=${props.locationData.city}`; + updateQueryUrl += `®ion=${props.locationData.region}`; + updateQueryUrl += `&country=${props.locationData.country}`; + updateQueryUrl += `&timezone=${props.locationData.timezone}`; + } + + let method = props.createNew ? "POST" : "PUT"; + + fetch(updateQueryUrl, { method: method }) + .then((response) => response.json()) + .then((data: AutomationsData) => { + props.setIsEditing(false); + props.setUpdatedAutomationData({ + id: data.id, + subject: data.subject || "", + query_to_run: data.query_to_run, + scheduling_request: data.scheduling_request, + schedule: cronToHumanReadableString(data.crontime), + crontime: data.crontime, + next: data.next, + }); + }); + }; + + function convertFrequencyToCron( + frequency: string, + timeRecurrence: string, + dayOfWeek?: number, + dayOfMonth?: string, + ) { + let cronString = ""; + + const minutes = timeRecurrence.split(":")[1].split(" ")[0]; + const period = timeRecurrence.split(":")[1].split(" ")[1]; + const rawHourAsNumber = Number(timeRecurrence.split(":")[0]); + const hours = + period === "PM" && rawHourAsNumber < 12 + ? String(rawHourAsNumber + 12) + : rawHourAsNumber; + + const dayOfWeekNumber = dayOfWeek ? dayOfWeek : "*"; + + switch (frequency) { + case "Day": + cronString = `${minutes} ${hours} * * *`; + break; + case "Week": + cronString = `${minutes} ${hours} * * ${dayOfWeekNumber}`; + break; + case "Month": + cronString = `${minutes} ${hours} ${dayOfMonth} * *`; + break; + } + + return cronString; + } + + return ( + + ); +} + +interface AutomationModificationFormProps { + form: UseFormReturn>; + onSubmit: (values: z.infer) => void; + create?: boolean; + isLoggedIn: boolean; + setShowLoginPrompt: (showLoginPrompt: boolean) => void; + authenticatedData: UserProfile | null; + locationData: LocationData | null; +} + +function AutomationModificationForm(props: AutomationModificationFormProps) { + const [isSaving, setIsSaving] = useState(false); + const { errors } = props.form.formState; + + function recommendationPill( + recommendationText: string, + onChange: (value: any, event: React.MouseEvent) => void, + ) { + return ( + + ); + } + + const recommendationPills = [ + "Make a picture of", + "Generate a summary of", + "Create a newsletter of", + "Notify me when", + ]; + + return ( +
+ { + props.onSubmit(values); + setIsSaving(true); + })} + className="space-y-8" + > + + Setup + + Emails will be sent to this address. Timezone and location data will be used + to schedule automations. + {props.locationData && + metadataMap(props.locationData, props.authenticatedData)} + + + {!props.create && ( + ( + + Subject + + This is the subject of the email you will receive. + + + + + + {errors.subject && ( + {errors.subject?.message} + )} + + )} + /> + )} + + ( + + Frequency + How often should this automation run? + + + {errors.subject && ( + {errors.everyBlah?.message} + )} + + )} + /> + {props.form.watch("everyBlah") === "Week" && ( + ( + + + Every week, on which day should this automation run? + + + + {errors.subject && ( + {errors.dayOfWeek?.message} + )} + + )} + /> + )} + {props.form.watch("everyBlah") === "Month" && ( + ( + + + Every month, on which day should the automation run? + + + + {errors.subject && ( + {errors.dayOfMonth?.message} + )} + + )} + /> + )} + {(props.form.watch("everyBlah") === "Day" || + props.form.watch("everyBlah") == "Week" || + props.form.watch("everyBlah") == "Month") && ( + ( + + Time + + On the days this automation runs, at what time should it run? + + + + {errors.subject && ( + {errors.timeRecurrence?.message} + )} + + )} + /> + )} + ( + + Instructions + What do you want Khoj to do? + {props.create && ( +
+ {recommendationPills.map((recommendation) => + recommendationPill(recommendation, field.onChange), + )} +
+ )} + +