diff --git a/src/khoj/interface/desktop/main_window.py b/src/khoj/interface/desktop/main_window.py index 4237ac17..751c0a64 100644 --- a/src/khoj/interface/desktop/main_window.py +++ b/src/khoj/interface/desktop/main_window.py @@ -194,6 +194,8 @@ class MainWindow(QtWidgets.QMainWindow): # Search Type (re)-Enabled if child.isChecked(): current_search_config = self.current_config["content-type"].get(child.search_type, {}) + if current_search_config == None: + current_search_config = {} default_search_config = self.get_default_config(search_type=child.search_type) self.new_config["content-type"][child.search_type.value] = merge_dicts( current_search_config, default_search_config diff --git a/src/khoj/interface/web/404.html b/src/khoj/interface/web/404.html new file mode 100644 index 00000000..7041ff80 --- /dev/null +++ b/src/khoj/interface/web/404.html @@ -0,0 +1,22 @@ + + + + Khoj: An AI Personal Assistant for your digital brain + + + + +
+

Oops, this is awkward. That page couldn't be found.

+
+ Go Home + + + + + diff --git a/src/khoj/interface/web/assets/config.css b/src/khoj/interface/web/assets/config.css deleted file mode 100644 index eff882ba..00000000 --- a/src/khoj/interface/web/assets/config.css +++ /dev/null @@ -1,29 +0,0 @@ -:root { - --primary-color: #ffffff; - --bold-color: #2073ee; - --complementary-color: #124408; - --accent-color-0: #57f0b5; -} - -input[type=text] { - width: 40%; -} - -div.config-element { - color: var(--bold-color); - margin: 8px; -} - -div.config-title { - font-weight: bold; -} - -span.config-element-value { - color: var(--complementary-color); - font-weight: normal; - cursor: pointer; -} - -button { - cursor: pointer; -} diff --git a/src/khoj/interface/web/assets/config.js b/src/khoj/interface/web/assets/config.js deleted file mode 100644 index ede4c65d..00000000 --- a/src/khoj/interface/web/assets/config.js +++ /dev/null @@ -1,125 +0,0 @@ -// Retrieve elements from the DOM. -var showConfig = document.getElementById("show-config"); -var configForm = document.getElementById("config-form"); -var regenerateButton = document.getElementById("config-regenerate"); - -// Global variables. -var rawConfig = {}; -var emptyValueDefault = "🖊️"; - -/** - * Fetch the existing config file. - */ -fetch("/api/config/data") - .then(response => response.json()) - .then(data => { - rawConfig = data; - configForm.style.display = "block"; - processChildren(configForm, data); - - var submitButton = document.createElement("button"); - submitButton.type = "submit"; - submitButton.innerHTML = "update"; - configForm.appendChild(submitButton); - - // The config form's submit handler. - configForm.addEventListener("submit", (event) => { - event.preventDefault(); - console.log(rawConfig); - fetch("/api/config/data", { - method: "POST", - credentials: "same-origin", - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(rawConfig) - }) - .then(response => response.json()) - .then(data => console.log(data)); - }); -}); - -/** - * The click handler for the Regenerate button. - */ -regenerateButton.addEventListener("click", (event) => { - event.preventDefault(); - regenerateButton.style.cursor = "progress"; - regenerateButton.disabled = true; - fetch("/api/update?force=true&client=web") - .then(response => response.json()) - .then(data => { - regenerateButton.style.cursor = "pointer"; - regenerateButton.disabled = false; - console.log(data); - }); -}) - -/** - * Adds config elements to the DOM representing the sub-components - * of one of the fields in the raw config file. - * @param {the parent element} element - * @param {the data to be rendered for this element and its children} data - */ -function processChildren(element, data) { - for (let key in data) { - var child = document.createElement("div"); - child.id = key; - child.className = "config-element"; - child.appendChild(document.createTextNode(key + ": ")); - if (data[key] === Object(data[key]) && !Array.isArray(data[key])) { - child.className+=" config-title"; - processChildren(child, data[key]); - } else { - child.appendChild(createValueNode(data, key)); - } - element.appendChild(child); - } -} - -/** - * Takes an element, and replaces it with an editable - * element with the same data in place. - * @param {the original element to be replaced} original - * @param {the source data to be rendered for the new element} data - * @param {the key for this input in the source data} key - */ -function makeElementEditable(original, data, key) { - original.addEventListener("click", () => { - var inputNewText = document.createElement("input"); - inputNewText.type = "text"; - inputNewText.className = "config-element-edit"; - inputNewText.value = (original.textContent == emptyValueDefault) ? "" : original.textContent; - fixInputOnFocusOut(inputNewText, data, key); - original.parentNode.replaceChild(inputNewText, original); - inputNewText.focus(); - }); -} - -/** - * Creates a node corresponding to the value of a config element. - * @param {the source data} data - * @param {the key corresponding to this node's data} key - * @returns A new element which corresponds to the value in some field. - */ -function createValueNode(data, key) { - var valueElement = document.createElement("span"); - valueElement.className = "config-element-value"; - valueElement.textContent = !data[key] ? emptyValueDefault : data[key]; - makeElementEditable(valueElement, data, key); - return valueElement; -} - -/** - * Replaces an existing input element with an element with the same data, which is not an input. - * If the input data for this element was changed, update the corresponding data in the raw config. - * @param {the original element to be replaced} original - * @param {the source data} data - * @param {the key corresponding to this node's data} key - */ -function fixInputOnFocusOut(original, data, key) { - original.addEventListener("blur", () => { - data[key] = (original.value != emptyValueDefault) ? original.value : ""; - original.parentNode.replaceChild(createValueNode(data, key), original); - }) -} diff --git a/src/khoj/interface/web/assets/icons/favicon-128x128.png b/src/khoj/interface/web/assets/icons/favicon-128x128.png deleted file mode 100644 index 7fba4f64..00000000 Binary files a/src/khoj/interface/web/assets/icons/favicon-128x128.png and /dev/null differ diff --git a/src/khoj/interface/web/assets/icons/favicon-144x144.ico b/src/khoj/interface/web/assets/icons/favicon-144x144.ico index 9641ed71..ed751b2d 100644 Binary files a/src/khoj/interface/web/assets/icons/favicon-144x144.ico and b/src/khoj/interface/web/assets/icons/favicon-144x144.ico differ diff --git a/src/khoj/interface/web/assets/icons/favicon-144x144.png b/src/khoj/interface/web/assets/icons/favicon-144x144.png index 6a1b7b4f..0e467e57 100644 Binary files a/src/khoj/interface/web/assets/icons/favicon-144x144.png and b/src/khoj/interface/web/assets/icons/favicon-144x144.png differ diff --git a/src/khoj/interface/web/assets/icons/favicon.icns b/src/khoj/interface/web/assets/icons/favicon.icns deleted file mode 100644 index 6e0c6c75..00000000 Binary files a/src/khoj/interface/web/assets/icons/favicon.icns and /dev/null differ diff --git a/src/khoj/interface/web/base_config.html b/src/khoj/interface/web/base_config.html new file mode 100644 index 00000000..c927abc8 --- /dev/null +++ b/src/khoj/interface/web/base_config.html @@ -0,0 +1,35 @@ + + + + + Khoj - Configure App + + + +
+

Configure Khoj

+
+

Ready?

+
+ + +
+
+
+
+ {% block content %} + {% endblock %} +
+ + + diff --git a/src/khoj/interface/web/base_data_integration.html b/src/khoj/interface/web/base_data_integration.html new file mode 100644 index 00000000..8917999d --- /dev/null +++ b/src/khoj/interface/web/base_data_integration.html @@ -0,0 +1,27 @@ + + + + Khoj: Configuration for data integrations + + + + +
+

Configure your data integrations for Khoj

+
+ Go back + +
+ {% block content %} + {% endblock %} +
+ + + + + diff --git a/src/khoj/interface/web/base_processor_integration.html b/src/khoj/interface/web/base_processor_integration.html new file mode 100644 index 00000000..ec4152f3 --- /dev/null +++ b/src/khoj/interface/web/base_processor_integration.html @@ -0,0 +1,27 @@ + + + + Khoj: Configuration for processor integrations + + + + +
+

Configure your processor integrations for Khoj

+
+ Go back + +
+ {% block content %} + {% endblock %} +
+ + + + + diff --git a/src/khoj/interface/web/config.html b/src/khoj/interface/web/config.html index 27de4aa9..e6b2c9c4 100644 --- a/src/khoj/interface/web/config.html +++ b/src/khoj/interface/web/config.html @@ -1,14 +1,32 @@ - - - - - - Khoj - Configure App - - -
-
- - - - +{% extends "base_config.html" %} +{% block content %} +

Content Types

+
+ + + + +
+

Processors

+ + +{% endblock %} diff --git a/src/khoj/interface/web/content_type_input.html b/src/khoj/interface/web/content_type_input.html new file mode 100644 index 00000000..9fe04957 --- /dev/null +++ b/src/khoj/interface/web/content_type_input.html @@ -0,0 +1,151 @@ +{% extends "base_data_integration.html" %} +{% block content %} +

{{ content_type }}

+
+ + + + + + + + + + + + + + + +
FieldValue
+ + + {% if current_config['input_files'] is none %} + + {% else %} + {% for input_file in current_config['input_files'] %} + + {% endfor %} + {% endif %} + + +
+ + + {% if current_config['input_filter'] is none %} + + {% else %} + {% for input_filter in current_config['input_filter'] %} + + {% endfor %} + {% endif %} + + +
+ +

You probably don't need to edit these.

+ + + + + + + + + + + + + + + + + + +
FieldValue
+ + + +
+ + + +
+ + + +
+ +
+ +{% endblock %} diff --git a/src/khoj/interface/web/khoj.webmanifest b/src/khoj/interface/web/khoj.webmanifest index d358cec4..31f806d7 100644 --- a/src/khoj/interface/web/khoj.webmanifest +++ b/src/khoj/interface/web/khoj.webmanifest @@ -5,7 +5,7 @@ "icons": [ { "src": "/static/assets/icons/favicon-144x144.png", - "sizes": "144x144", + "sizes": "128x128", "type": "image/png" } ], diff --git a/src/khoj/interface/web/khoj_chat.webmanifest b/src/khoj/interface/web/khoj_chat.webmanifest index 82d553e8..c19f420d 100644 --- a/src/khoj/interface/web/khoj_chat.webmanifest +++ b/src/khoj/interface/web/khoj_chat.webmanifest @@ -5,7 +5,7 @@ "icons": [ { "src": "/static/assets/icons/favicon-144x144.png", - "sizes": "144x144", + "sizes": "128x128", "type": "image/png" } ], diff --git a/src/khoj/interface/web/processor_conversation_input.html b/src/khoj/interface/web/processor_conversation_input.html new file mode 100644 index 00000000..0916d307 --- /dev/null +++ b/src/khoj/interface/web/processor_conversation_input.html @@ -0,0 +1,72 @@ +{% extends "base_processor_integration.html" %} +{% block content %} +

Conversation

+
+ + + + + +
+ + + +
+ +

You probably don't need to edit these.

+ + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ +
+ +{% endblock %} diff --git a/src/khoj/routers/api.py b/src/khoj/routers/api.py index bf165a1e..cc25ee39 100644 --- a/src/khoj/routers/api.py +++ b/src/khoj/routers/api.py @@ -15,9 +15,10 @@ from khoj.processor.conversation.gpt import converse, extract_questions from khoj.processor.conversation.utils import message_to_log, message_to_prompt from khoj.search_type import image_search, text_search from khoj.utils.helpers import log_telemetry, timer -from khoj.utils.rawconfig import FullConfig, SearchResponse +from khoj.utils.rawconfig import FullConfig, SearchResponse, TextContentConfig, ConversationProcessorConfig from khoj.utils.state import SearchType from khoj.utils import state, constants +from khoj.utils.yaml import save_config_to_file_updated_state # Initialize Router api = APIRouter() @@ -62,6 +63,18 @@ async def set_config_data(updated_config: FullConfig): return state.config +@api.post("/config/data/content_type/{content_type}") +async def set_content_config_data(content_type: str, updated_config: TextContentConfig): + state.config.content_type[content_type] = updated_config + save_config_to_file_updated_state() + + +@api.post("/config/data/processor/conversation") +async def set_processor_conversation_config_data(updated_config: ConversationProcessorConfig): + state.config.processor.conversation = updated_config + save_config_to_file_updated_state() + + @api.get("/search", response_model=List[SearchResponse]) def search( q: str, diff --git a/src/khoj/routers/web_client.py b/src/khoj/routers/web_client.py index c452efdf..0a64efc7 100644 --- a/src/khoj/routers/web_client.py +++ b/src/khoj/routers/web_client.py @@ -3,15 +3,23 @@ from fastapi import APIRouter from fastapi import Request from fastapi.responses import HTMLResponse, FileResponse from fastapi.templating import Jinja2Templates +from khoj.utils.rawconfig import TextContentConfig, ConversationProcessorConfig # Internal Packages -from khoj.utils import constants +from khoj.utils import constants, state + +import logging +import json + +logger = logging.getLogger("khoj") # Initialize Router web_client = APIRouter() templates = Jinja2Templates(directory=constants.web_directory) +VALID_CONTENT_TYPES = ["org", "ledger", "image", "music", "markdown", "pdf"] + # Create Routes @web_client.get("/", response_class=FileResponse) @@ -24,6 +32,67 @@ def config_page(request: Request): return templates.TemplateResponse("config.html", context={"request": request}) +@web_client.get("/test-child", response_class=HTMLResponse) +def test_child(request: Request): + return templates.TemplateResponse("child0.html", context={"request": request, "config": constants.default_config}) + + +@web_client.get("/config/content_type/{content_type}", response_class=HTMLResponse) +def content_config_page(request: Request, content_type: str): + if content_type not in VALID_CONTENT_TYPES: + return templates.TemplateResponse("config.html", context={"request": request}) + + default_copy = constants.default_config.copy() + default_content_type = default_copy["content-type"][content_type] + + default_config = TextContentConfig( + compressed_jsonl=default_content_type["compressed-jsonl"], + embeddings_file=default_content_type["embeddings-file"], + ) + current_config = ( + state.config.content_type[content_type] + if state.config.content_type[content_type] is not None + else default_config + ) + current_config = json.loads(current_config.json()) + + return templates.TemplateResponse( + "content_type_input.html", + context={ + "request": request, + "current_config": current_config, + "content_type": content_type, + }, + ) + + +@web_client.get("/config/processor/conversation", response_class=HTMLResponse) +def conversation_processor_config_page(request: Request): + default_copy = constants.default_config.copy() + default_processor_config = default_copy["processor"]["conversation"] + default_processor_config = ConversationProcessorConfig( + openai_api_key="", + model=default_processor_config["model"], + conversation_logfile=default_processor_config["conversation-logfile"], + chat_model=default_processor_config["chat-model"], + ) + + current_processor_conversation_config = ( + state.config.processor.conversation + if state.config.processor.conversation is not None + else default_processor_config + ) + current_processor_conversation_config = json.loads(current_processor_conversation_config.json()) + + return templates.TemplateResponse( + "processor_conversation_input.html", + context={ + "request": request, + "current_config": current_processor_conversation_config, + }, + ) + + @web_client.get("/chat", response_class=FileResponse) def chat_page(): return FileResponse(constants.web_directory / "chat.html") diff --git a/src/khoj/utils/constants.py b/src/khoj/utils/constants.py index 87eb07ac..8958a57a 100644 --- a/src/khoj/utils/constants.py +++ b/src/khoj/utils/constants.py @@ -14,7 +14,7 @@ default_config = { "input-filter": None, "compressed-jsonl": "~/.khoj/content/org/org.jsonl.gz", "embeddings-file": "~/.khoj/content/org/org_embeddings.pt", - "index_heading_entries": False, + "index-heading-entries": False, }, "markdown": { "input-files": None, @@ -66,6 +66,7 @@ default_config = { "openai-api-key": None, "model": "text-davinci-003", "conversation-logfile": "~/.khoj/processor/conversation/conversation_logs.json", + "chat-model": "gpt-3.5-turbo", } }, } diff --git a/src/khoj/utils/rawconfig.py b/src/khoj/utils/rawconfig.py index 72d82ce9..af254168 100644 --- a/src/khoj/utils/rawconfig.py +++ b/src/khoj/utils/rawconfig.py @@ -15,6 +15,12 @@ class ConfigBase(BaseModel): alias_generator = to_snake_case_from_dash allow_population_by_field_name = True + def __getitem__(self, item): + return getattr(self, item) + + def __setitem__(self, key, value): + return setattr(self, key, value) + class TextContentConfig(ConfigBase): input_files: Optional[List[Path]] diff --git a/src/khoj/utils/yaml.py b/src/khoj/utils/yaml.py index c91f4fef..abfe270a 100644 --- a/src/khoj/utils/yaml.py +++ b/src/khoj/utils/yaml.py @@ -6,12 +6,20 @@ import yaml # Internal Packages from khoj.utils.rawconfig import FullConfig +from khoj.utils import state # Do not emit tags when dumping to YAML yaml.emitter.Emitter.process_tag = lambda self, *args, **kwargs: None # type: ignore[assignment] +def save_config_to_file_updated_state(): + with open(state.config_file, "w") as outfile: + yaml.dump(yaml.safe_load(state.config.json(by_alias=True)), outfile) + outfile.close() + return state.config + + def save_config_to_file(yaml_config: dict, yaml_config_file: Path): "Write config to YML file" # Create output directory, if it doesn't exist