diff --git a/README.md b/README.md
index 39c2965c..5868f0a6 100644
--- a/README.md
+++ b/README.md
@@ -73,13 +73,17 @@ https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f
Description
-- Install Khoj via `pip` and start Khoj backend in non-gui mode
-- Install Khoj plugin via Community Plugins settings pane on Obsidian app
-- Check the new Khoj plugin settings
-- Let Khoj backend index the markdown, pdf, Github markdown files in the current Vault
-- Open Khoj plugin on Obsidian via Search button on Left Pane
-- Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
-- Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
+1. Install Khoj via `pip` and start Khoj backend in a terminal (Run `khoj`)
+ ```
+ python -m pip install khoj-assistant
+ khoj
+ ```
+2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
+ - Check the new Khoj plugin settings
+ - Let Khoj backend index the markdown, pdf, Github markdown files in the current Vault
+ - Open Khoj plugin on Obsidian via Search button on Left Pane
+ - Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
+ - Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
### Khoj in Emacs, Browser
@@ -160,7 +164,7 @@ The optional steps below allow using Khoj from within an existing application li
- **Khoj via Emacs**
- Run `M-x khoj `
- **Khoj via Web**
- - Open via desktop interface or directly
+ - Open directly
- **Khoj via API**
- See the Khoj FastAPI [Swagger Docs](http://localhost:8000/docs), [ReDocs](http://localhost:8000/redocs)
@@ -308,8 +312,7 @@ pip install --upgrade --pre khoj-assistant
### Set your OpenAI API key in Khoj
If you want, Khoj can be configured to use OpenAI for search and chat.
Add your OpenAI API to Khoj by using either of the two options below:
- - Open the Khoj desktop GUI, add your [OpenAI API key](https://beta.openai.com/account/api-keys) and click *Configure*
- Ensure khoj is started **without** the `--no-gui` flag. Check your system tray to see if Khoj is minimized there.
+ - Open your [Khoj settings](http://localhost:8000/config/processor/conversation), add your OpenAI API key, and click *Save*. Then go to your [Khoj settings](http://localhost:8000/config) and click `Configure`. This will refresh Khoj with your OpenAI API key.
- Set `openai-api-key` field under `processor.conversation` section in your `khoj.yml`[^1] to your [OpenAI API key](https://beta.openai.com/account/api-keys) and restart khoj:
```diff
processor:
@@ -388,7 +391,7 @@ pip install -e .[dev]
khoj -vv
```
2. Configure Khoj
- - **Via GUI**: Add files, directories to index in the GUI window that pops up on starting Khoj, then Click Configure
+ - **Via the Settings UI**: Add files, directories to index the [Khoj settings](http://localhost:8000/config) UI once Khoj has started up. Once you've saved all your settings, click `Configure`.
- **Manually**:
- Copy the `config/khoj_sample.yml` to `~/.khoj/khoj.yml`
- Set `input-files` or `input-filter` in each relevant `content-type` section of `~/.khoj/khoj.yml`
diff --git a/docker-compose.yml b/docker-compose.yml
index 03b74444..84f8d3f3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -27,4 +27,4 @@ services:
- ./tests/data/embeddings/:/data/embeddings/
- ./tests/data/models/:/data/models/
# Use 0.0.0.0 to explicitly set the host ip for the service on the container. https://pythonspeed.com/articles/docker-connection-refused/
- command: --no-gui --host="0.0.0.0" --port=8000 -c=config/khoj_docker.yml -vv
+ command: --host="0.0.0.0" --port=8000 -c=config/khoj_docker.yml -vv
diff --git a/src/interface/emacs/khoj.el b/src/interface/emacs/khoj.el
index 41ac0aaf..6f08ab9c 100644
--- a/src/interface/emacs/khoj.el
+++ b/src/interface/emacs/khoj.el
@@ -197,7 +197,7 @@ Use `which-key` if available, else display simple message in echo area"
:type 'string
:group 'khoj)
-(defcustom khoj-server-args '("--no-gui")
+(defcustom khoj-server-args '()
"Arguments to pass to Khoj server on startup."
:type '(repeat string)
:group 'khoj)
diff --git a/src/interface/obsidian/README.md b/src/interface/obsidian/README.md
index 1cb72006..f2d664d3 100644
--- a/src/interface/obsidian/README.md
+++ b/src/interface/obsidian/README.md
@@ -40,13 +40,16 @@ https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f
Description
-1. Install Khoj via `pip` and start Khoj backend in non-gui mode
+1. Install Khoj via `pip` and start Khoj backend
+ ```shell
+ python -m pip install khoj-assistant && khoj
+ ```
2. Install Khoj plugin via Community Plugins settings pane on Obsidian app
-3. Check the new Khoj plugin settings
-4. Wait for Khoj backend to index markdown, PDF files in the current Vault
-5. Open Khoj plugin on Obsidian via Search button on Left Pane
-6. Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
-7. Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
+ - Check the new Khoj plugin settings
+ - Wait for Khoj backend to index markdown, PDF files in the current Vault
+ - Open Khoj plugin on Obsidian via Search button on Left Pane
+ - Search \"*Announce plugin to folks*\" in the [Obsidian Plugin docs](https://marcus.se.net/obsidian-plugin-docs/)
+ - Jump to the [search result](https://marcus.se.net/obsidian-plugin-docs/publishing/submit-your-plugin)
@@ -65,12 +68,12 @@ https://github.com/khoj-ai/khoj/assets/6413477/3e33d8ea-25bb-46c8-a3bf-c92f78d0f
Open terminal/cmd and run below command to install and start the khoj backend
- On Linux/MacOS
```shell
- python -m pip install khoj-assistant && khoj --no-gui
+ python -m pip install khoj-assistant && khoj
```
- On Windows
```shell
- py -m pip install khoj-assistant && khoj --no-gui
+ py -m pip install khoj-assistant && khoj
```
### 2. Setup Plugin
@@ -96,7 +99,7 @@ See [Khoj Chat](https://github.com/khoj-ai/khoj/tree/master/#Khoj-Chat) for more
### Search
Click the *Khoj search* icon 🔎 on the [Ribbon](https://help.obsidian.md/User+interface/Workspace/Ribbon) or run *Khoj: Search* from the [Command Palette](https://help.obsidian.md/Plugins/Command+palette)
-*Note: Ensure the khoj server is running in the background before searching. Execute `khoj --no-gui` in your terminal if it is not already running*
+*Note: Ensure the khoj server is running in the background before searching. Execute `khoj` in your terminal if it is not already running*
https://user-images.githubusercontent.com/6413477/218801155-cd67e8b4-a770-404a-8179-d6b61caa0f93.mp4
diff --git a/src/khoj/configure.py b/src/khoj/configure.py
index 050cf069..de543349 100644
--- a/src/khoj/configure.py
+++ b/src/khoj/configure.py
@@ -34,11 +34,13 @@ logger = logging.getLogger(__name__)
def configure_server(args, required=False):
if args.config is None:
if required:
- logger.error(f"Exiting as Khoj is not configured.\nConfigure it via GUI or by editing {state.config_file}.")
+ logger.error(
+ f"Exiting as Khoj is not configured.\nConfigure it via http://localhost:8000/config or by editing {state.config_file}."
+ )
sys.exit(1)
else:
logger.warning(
- f"Khoj is not configured.\nConfigure it via khoj GUI, plugins or by editing {state.config_file}."
+ f"Khoj is not configured.\nConfigure it via http://localhost:8000/config, plugins or by editing {state.config_file}."
)
return
else:
diff --git a/src/khoj/interface/desktop/file_browser.py b/src/khoj/interface/desktop/file_browser.py
deleted file mode 100644
index d7071664..00000000
--- a/src/khoj/interface/desktop/file_browser.py
+++ /dev/null
@@ -1,77 +0,0 @@
-# External Packages
-from PyQt6 import QtWidgets
-from PyQt6.QtCore import QDir
-
-# Internal Packages
-from khoj.utils.config import SearchType
-from khoj.utils.helpers import is_none_or_empty
-
-
-class FileBrowser(QtWidgets.QWidget):
- def __init__(self, title, search_type: SearchType = None, default_files: list = []):
- QtWidgets.QWidget.__init__(self)
- layout = QtWidgets.QHBoxLayout()
- self.setLayout(layout)
- self.search_type = search_type
-
- self.filter_name = self.getFileFilter(search_type)
- self.dirpath = QDir.homePath()
-
- self.label = QtWidgets.QLabel()
- self.label.setText(title)
- self.label.setFixedWidth(95)
- self.label.setWordWrap(True)
- layout.addWidget(self.label)
-
- self.lineEdit = QtWidgets.QPlainTextEdit(self)
- self.lineEdit.setFixedWidth(330)
- self.setFiles(default_files)
- self.lineEdit.setFixedHeight(min(7 + 20 * len(self.lineEdit.toPlainText().split("\n")), 90))
- self.lineEdit.textChanged.connect(self.updateFieldHeight) # type: ignore[attr-defined]
- layout.addWidget(self.lineEdit)
-
- self.button = QtWidgets.QPushButton("Add")
- self.button.clicked.connect(self.storeFilesSelectedInFileDialog) # type: ignore[attr-defined]
- layout.addWidget(self.button)
- layout.addStretch()
-
- def getFileFilter(self, search_type):
- if search_type == SearchType.Org:
- return "Org-Mode Files (*.org)"
- elif search_type == SearchType.Ledger:
- return "Beancount Files (*.bean *.beancount)"
- elif search_type == SearchType.Markdown:
- return "Markdown Files (*.md *.markdown)"
- elif search_type == SearchType.Pdf:
- return "Pdf Files (*.pdf)"
- elif search_type == SearchType.Music:
- return "Org-Music Files (*.org)"
- elif search_type == SearchType.Image:
- return "Images (*.jp[e]g)"
-
- def storeFilesSelectedInFileDialog(self):
- filepaths = self.getPaths()
- if self.search_type == SearchType.Image:
- filepaths.append(
- QtWidgets.QFileDialog.getExistingDirectory(self, caption="Choose Folder", directory=self.dirpath)
- )
- else:
- filepaths.extend(
- QtWidgets.QFileDialog.getOpenFileNames(
- self, caption="Choose Files", directory=self.dirpath, filter=self.filter_name
- )[0]
- )
- self.setFiles(filepaths)
-
- def setFiles(self, paths: list):
- self.filepaths = [path for path in paths if not is_none_or_empty(path)]
- self.lineEdit.setPlainText("\n".join(self.filepaths))
-
- def getPaths(self) -> list:
- if self.lineEdit.toPlainText() == "":
- return []
- else:
- return self.lineEdit.toPlainText().split("\n")
-
- def updateFieldHeight(self):
- self.lineEdit.setFixedHeight(min(7 + 20 * len(self.lineEdit.toPlainText().split("\n")), 90))
diff --git a/src/khoj/interface/desktop/labelled_text_field.py b/src/khoj/interface/desktop/labelled_text_field.py
deleted file mode 100644
index a897ee48..00000000
--- a/src/khoj/interface/desktop/labelled_text_field.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# External Packages
-from PyQt6 import QtWidgets
-
-# Internal Packages
-from khoj.utils.config import ProcessorType
-from khoj.utils.config import SearchType
-
-
-class LabelledTextField(QtWidgets.QWidget):
- def __init__(
- self, title, search_type: SearchType = None, processor_type: ProcessorType = None, default_value: str = None
- ):
- QtWidgets.QWidget.__init__(self)
- layout = QtWidgets.QHBoxLayout()
- self.setLayout(layout)
- self.processor_type = processor_type
- self.search_type = search_type
-
- self.label = QtWidgets.QLabel()
- self.label.setText(title)
- self.label.setFixedWidth(95)
- self.label.setWordWrap(True)
- layout.addWidget(self.label)
-
- self.input_field = QtWidgets.QTextEdit(self)
- self.input_field.setFixedWidth(410)
- self.input_field.setFixedHeight(27)
- self.input_field.setText(default_value)
-
- layout.addWidget(self.input_field)
- layout.addStretch()
diff --git a/src/khoj/interface/desktop/main_window.py b/src/khoj/interface/desktop/main_window.py
index f4ca7f8c..6aef7be8 100644
--- a/src/khoj/interface/desktop/main_window.py
+++ b/src/khoj/interface/desktop/main_window.py
@@ -1,52 +1,22 @@
# Standard Packages
-from enum import Enum
-from pathlib import Path
-from copy import deepcopy
import webbrowser
# External Packages
from PyQt6 import QtGui, QtWidgets
-from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal
+from PyQt6.QtCore import Qt
# Internal Packages
-from khoj.configure import configure_server
-from khoj.interface.desktop.file_browser import FileBrowser
-from khoj.interface.desktop.labelled_text_field import LabelledTextField
-from khoj.utils import constants, state, yaml as yaml_utils
-from khoj.utils.cli import cli
-from khoj.utils.config import SearchType, ProcessorType
-from khoj.utils.helpers import merge_dicts, resolve_absolute_path
+from khoj.utils import constants
class MainWindow(QtWidgets.QMainWindow):
- """Create Window to Configure Khoj
- Allow user to
- 1. Configure content types to search
- 2. Configure conversation processor
- 3. Save the configuration to khoj.yml
- """
+ """Create Window to Navigate users to the web UI"""
- def __init__(self, config_file: Path):
+ def __init__(self, host: str, port: int):
super(MainWindow, self).__init__()
- self.config_file = config_file
- # Set regenerate flag to regenerate embeddings everytime user clicks configure
- if state.cli_args:
- state.cli_args += ["--regenerate"]
- else:
- state.cli_args = ["--regenerate"]
-
- # Load config from existing config, if exists, else load from default config
- if resolve_absolute_path(self.config_file).exists():
- self.first_run = False
- self.current_config = yaml_utils.load_config_from_file(self.config_file)
- else:
- self.first_run = True
- self.current_config = deepcopy(constants.default_config)
- self.new_config = self.current_config
# Initialize Configure Window
self.setWindowTitle("Khoj")
- self.setFixedWidth(600)
# Set Window Icon
icon_path = constants.web_directory / "assets/icons/favicon-128x128.png"
@@ -55,24 +25,14 @@ class MainWindow(QtWidgets.QMainWindow):
# Initialize Configure Window Layout
self.wlayout = QtWidgets.QVBoxLayout()
- # Add Settings Panels for each Search Type to Configure Window Layout
- self.search_settings_panels = []
- for search_type in SearchType:
- current_content_config = self.current_config["content-type"].get(
- search_type, None
- ) or self.get_default_config(search_type=search_type)
- self.search_settings_panels += [self.add_settings_panel(current_content_config, search_type)]
- # Add Conversation Processor Panel to Configure Screen
- self.processor_settings_panels = []
- conversation_type = ProcessorType.Conversation
- if self.current_config["processor"] and conversation_type in self.current_config["processor"]:
- current_conversation_config = self.current_config["processor"][conversation_type]
- else:
- current_conversation_config = self.get_default_config(processor_type=conversation_type)
- self.processor_settings_panels += [self.add_processor_panel(current_conversation_config, conversation_type)]
+ # Add a Label that says "Khoj Configuration" to the Window
+ self.wlayout.addWidget(QtWidgets.QLabel("Welcome to Khoj"))
- # Add Action Buttons Panel
- self.add_action_panel()
+ # Add a Button to open the Web UI at http://host:port/config
+ self.open_web_ui_button = QtWidgets.QPushButton("Open Web UI")
+ self.open_web_ui_button.clicked.connect(lambda: webbrowser.open(f"http://{host}:{port}/config"))
+
+ self.wlayout.addWidget(self.open_web_ui_button)
# Set the central widget of the Window. Widget will expand
# to take up all the space in the window by default.
@@ -81,250 +41,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.setCentralWidget(self.config_window)
self.position_window()
- def add_settings_panel(self, current_content_config: dict, search_type: SearchType):
- "Add Settings Panel for specified Search Type. Toggle Editable Search Types"
- # Get current files from config for given search type
- if search_type == SearchType.Image:
- current_content_files = current_content_config.get("input-directories", [])
- file_input_text = f"{search_type.name} Folders"
- elif search_type == SearchType.Github:
- return self.add_github_settings_panel(current_content_config, SearchType.Github)
- else:
- current_content_files = current_content_config.get("input-files", [])
- file_input_text = f"{search_type.name} Files"
-
- # Create widgets to display settings for given search type
- search_type_settings = QtWidgets.QWidget()
- search_type_layout = QtWidgets.QVBoxLayout(search_type_settings)
- enable_search_type = SearchCheckBox(f"Search {search_type.name}", search_type)
- # Add file browser to set input files for given search type
- input_files = FileBrowser(file_input_text, search_type, current_content_files or [])
-
- # Set enabled/disabled based on checkbox state
- enable_search_type.setChecked(current_content_files is not None and len(current_content_files) > 0)
- input_files.setEnabled(enable_search_type.isChecked())
- enable_search_type.stateChanged.connect(lambda _: input_files.setEnabled(enable_search_type.isChecked())) # type: ignore[attr-defined]
-
- # Add setting widgets for given search type to panel
- search_type_layout.addWidget(enable_search_type)
- search_type_layout.addWidget(input_files)
- self.wlayout.addWidget(search_type_settings)
-
- return search_type_settings
-
- def add_github_settings_panel(self, current_content_config: dict, search_type: SearchType):
- search_type_settings = QtWidgets.QWidget()
- search_type_layout = QtWidgets.QVBoxLayout(search_type_settings)
- enable_search_type = SearchCheckBox(f"Search {search_type.name}", search_type)
- # Add labelled text input field
- input_fields = []
-
- fields = ["pat-token", "repo-name", "repo-owner", "repo-branch"]
- active = False
- for field in fields:
- field_value = current_content_config.get(field, None)
- input_field = LabelledTextField(field, search_type=search_type, default_value=field_value)
- search_type_layout.addWidget(input_field)
- input_fields += [input_field]
- if field_value:
- active = True
-
- # Set enabled/disabled based on checkbox state
- enable_search_type.setChecked(active)
- for input_field in input_fields:
- input_field.setEnabled(enable_search_type.isChecked())
- enable_search_type.stateChanged.connect(lambda _: [input_field.setEnabled(enable_search_type.isChecked()) for input_field in input_fields]) # type: ignore[attr-defined]
-
- # Add setting widgets for given search type to panel
- search_type_layout.addWidget(enable_search_type)
- for input_field in input_fields:
- search_type_layout.addWidget(input_field)
- self.wlayout.addWidget(search_type_settings)
-
- return search_type_settings
-
- def add_processor_panel(self, current_conversation_config: dict, processor_type: ProcessorType):
- "Add Conversation Processor Panel"
- # Get current settings from config for given processor type
- current_openai_api_key = current_conversation_config.get("openai-api-key", None)
-
- # Create widgets to display settings for given processor type
- processor_type_settings = QtWidgets.QWidget()
- processor_type_layout = QtWidgets.QVBoxLayout(processor_type_settings)
- enable_conversation = ProcessorCheckBox(f"Conversation", processor_type)
- # Add file browser to set input files for given processor type
- input_field = LabelledTextField(
- "OpenAI API Key", processor_type=processor_type, default_value=current_openai_api_key
- )
-
- # Set enabled/disabled based on checkbox state
- enable_conversation.setChecked(current_openai_api_key is not None)
- input_field.setEnabled(enable_conversation.isChecked())
- enable_conversation.stateChanged.connect(lambda _: input_field.setEnabled(enable_conversation.isChecked())) # type: ignore[attr-defined]
-
- # Add setting widgets for given processor type to panel
- processor_type_layout.addWidget(enable_conversation)
- processor_type_layout.addWidget(input_field)
- self.wlayout.addWidget(processor_type_settings)
-
- return processor_type_settings
-
- def add_action_panel(self):
- "Add Action Panel"
- # Button to Save Settings
- action_bar = QtWidgets.QWidget()
- action_bar_layout = QtWidgets.QHBoxLayout(action_bar)
-
- self.configure_button = QtWidgets.QPushButton("Configure", clicked=self.configure_app)
- self.search_button = QtWidgets.QPushButton(
- "Search", clicked=lambda: webbrowser.open(f"http://{state.host}:{state.port}/")
- )
- self.search_button.setEnabled(not self.first_run)
-
- action_bar_layout.addWidget(self.configure_button)
- action_bar_layout.addWidget(self.search_button)
- self.wlayout.addWidget(action_bar)
-
- def get_default_config(self, search_type: SearchType = None, processor_type: ProcessorType = None):
- "Get default config"
- config = constants.default_config
- if search_type:
- return config["content-type"][search_type] # type: ignore
- elif processor_type:
- return config["processor"][processor_type] # type: ignore
- else:
- return config
-
- def add_error_message(self, message: str):
- "Add Error Message to Configure Screen"
- # Remove any existing error messages
- for message_prefix in ErrorType:
- for i in reversed(range(self.wlayout.count())):
- current_widget = self.wlayout.itemAt(i).widget()
- if isinstance(current_widget, QtWidgets.QLabel) and current_widget.text().startswith(
- message_prefix.value
- ):
- self.wlayout.removeWidget(current_widget)
- current_widget.deleteLater()
-
- # Add new error message
- if message:
- error_message = QtWidgets.QLabel()
- error_message.setWordWrap(True)
- error_message.setText(message)
- error_message.setStyleSheet("color: red")
- self.wlayout.addWidget(error_message)
-
- def update_search_settings(self):
- "Update config with search settings from UI"
- for settings_panel in self.search_settings_panels:
- for child in settings_panel.children():
- if not isinstance(child, (SearchCheckBox, FileBrowser, LabelledTextField)):
- continue
- if isinstance(child, SearchCheckBox):
- # Search Type Disabled
- if not child.isChecked() and child.search_type in self.new_config["content-type"]:
- del self.new_config["content-type"][child.search_type]
- # 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
- )
- elif isinstance(child, FileBrowser) and child.search_type in self.new_config["content-type"]:
- if child.search_type.value == SearchType.Image:
- self.new_config["content-type"][child.search_type.value]["input-directories"] = (
- child.getPaths() if child.getPaths() != [] else None
- )
- else:
- self.new_config["content-type"][child.search_type.value]["input-files"] = (
- child.getPaths() if child.getPaths() != [] else None
- )
- elif isinstance(child, LabelledTextField):
- self.new_config["content-type"][child.search_type.value][
- child.label.text()
- ] = child.input_field.toPlainText()
-
- def update_processor_settings(self):
- "Update config with conversation settings from UI"
- for settings_panel in self.processor_settings_panels:
- for child in settings_panel.children():
- if not isinstance(child, (ProcessorCheckBox, LabelledTextField)):
- continue
- if isinstance(child, ProcessorCheckBox):
- # Processor Type Disabled
- if not child.isChecked() and child.processor_type in self.new_config["processor"]:
- del self.new_config["processor"][child.processor_type]
- # Processor Type (re)-Enabled
- if child.isChecked():
- current_processor_config = self.current_config["processor"].get(child.processor_type, {})
- default_processor_config = self.get_default_config(processor_type=child.processor_type)
- self.new_config["processor"][child.processor_type.value] = merge_dicts(
- current_processor_config, default_processor_config
- )
- elif isinstance(child, LabelledTextField) and child.processor_type in self.new_config["processor"]:
- if child.processor_type == ProcessorType.Conversation:
- self.new_config["processor"][child.processor_type.value]["openai-api-key"] = (
- child.input_field.toPlainText() if child.input_field.toPlainText() != "" else None
- )
-
- def save_settings_to_file(self) -> bool:
- "Save validated settings to file"
- # Validate config before writing to file
- try:
- yaml_utils.parse_config_from_string(self.new_config)
- except Exception as e:
- print(f"Error validating config: {e}")
- self.add_error_message(f"{ErrorType.ConfigValidationError.value}: {e}")
- return False
-
- # Save the config to app config file
- self.add_error_message(None)
- yaml_utils.save_config_to_file(self.new_config, self.config_file)
- return True
-
- def load_updated_settings(self):
- "Hot swap to use the updated config from config file"
- # Load parsed, validated config from app config file
- args = cli(state.cli_args)
- self.current_config = self.new_config
-
- # Configure server with loaded config
- configure_server(args, required=True)
-
- def configure_app(self):
- "Save the new settings to khoj.yml. Reload app with updated settings"
- self.update_search_settings()
- self.update_processor_settings()
- if self.save_settings_to_file():
- # Setup thread to load updated settings in background
- self.thread = QThread()
- self.settings_loader = SettingsLoader(self.load_updated_settings)
- self.settings_loader.moveToThread(self.thread)
-
- # Connect slots and signals for thread
- self.thread.started.connect(self.settings_loader.run)
- self.settings_loader.finished.connect(self.thread.quit)
- self.settings_loader.finished.connect(self.settings_loader.deleteLater)
- self.settings_loader.error.connect(self.add_error_message)
- self.thread.finished.connect(self.thread.deleteLater)
-
- # Start thread
- self.thread.start()
-
- # Disable Save Button
- self.search_button.setEnabled(False)
- self.configure_button.setEnabled(False)
- self.configure_button.setText("Configuring...")
-
- # Reset UI
- self.thread.finished.connect(lambda: self.configure_button.setText("Configure"))
- self.thread.finished.connect(lambda: self.configure_button.setEnabled(True))
- self.thread.finished.connect(lambda: self.search_button.setEnabled(True))
-
def position_window(self):
"Position the window at center of X axis and near top on Y axis"
window_rectangle = self.geometry()
@@ -338,41 +54,3 @@ class MainWindow(QtWidgets.QMainWindow):
self.setWindowState(Qt.WindowState.WindowActive)
self.activateWindow() # For Bringing to Top on Windows
self.raise_() # For Bringing to Top from Minimized State on OSX
-
-
-class SettingsLoader(QObject):
- "Load Settings Thread"
- finished = pyqtSignal()
- error = pyqtSignal(str)
-
- def __init__(self, load_settings_func):
- super(SettingsLoader, self).__init__()
- self.load_settings_func = load_settings_func
-
- def run(self):
- "Load Settings"
- try:
- self.load_settings_func()
- except FileNotFoundError as e:
- self.error.emit(f"{ErrorType.ConfigLoadingError.value}: {e}")
- else:
- self.error.emit(None)
- self.finished.emit()
-
-
-class SearchCheckBox(QtWidgets.QCheckBox):
- def __init__(self, text, search_type: SearchType, parent=None):
- self.search_type = search_type
- super(SearchCheckBox, self).__init__(text, parent=parent)
-
-
-class ProcessorCheckBox(QtWidgets.QCheckBox):
- def __init__(self, text, processor_type: ProcessorType, parent=None):
- self.processor_type = processor_type
- super(ProcessorCheckBox, self).__init__(text, parent=parent)
-
-
-class ErrorType(Enum):
- "Error Types"
- ConfigLoadingError = "Config Loading Error"
- ConfigValidationError = "Config Validation Error"
diff --git a/src/khoj/interface/desktop/system_tray.py b/src/khoj/interface/desktop/system_tray.py
index 526d7da6..048b2772 100644
--- a/src/khoj/interface/desktop/system_tray.py
+++ b/src/khoj/interface/desktop/system_tray.py
@@ -26,7 +26,8 @@ def create_system_tray(gui: QtWidgets.QApplication, main_window: MainWindow):
menu = QtWidgets.QMenu()
menu_actions = [
("Search", lambda: webbrowser.open(f"http://{state.host}:{state.port}/")),
- ("Configure", main_window.show_on_top),
+ ("Configure", lambda: webbrowser.open(f"http://{state.host}:{state.port}/config")),
+ ("App", main_window.show),
("Quit", gui.quit),
]
diff --git a/src/khoj/main.py b/src/khoj/main.py
index 5c2c71c0..b6347c53 100644
--- a/src/khoj/main.py
+++ b/src/khoj/main.py
@@ -6,6 +6,7 @@ import logging
import threading
import warnings
from platform import system
+import webbrowser
# Ignore non-actionable warnings
warnings.filterwarnings("ignore", message=r"snapshot_download.py has been made private", category=FutureWarning)
@@ -63,17 +64,20 @@ def run():
logger.info("🌘 Starting Khoj")
- if args.no_gui:
- # Setup task scheduler
- poll_task_scheduler()
+ if not args.gui:
+ if not state.demo:
+ # Setup task scheduler
+ poll_task_scheduler()
+
# Start Server
configure_server(args, required=False)
configure_routes(app)
start_server(app, host=args.host, port=args.port, socket=args.socket)
else:
+ logger.warning("🚧 GUI is being deprecated and may not work as expected. Starting...")
# Setup GUI
gui = QtWidgets.QApplication([])
- main_window = MainWindow(args.config_file)
+ main_window = MainWindow(args.host, args.port)
# System tray is only available on Windows, MacOS.
# On Linux (Gnome) the System tray is not supported.
@@ -89,6 +93,13 @@ def run():
configure_routes(app)
server = ServerThread(app, args.host, args.port, args.socket)
+ url = f"http://{args.host}:{args.port}"
+ logger.info(f"🌗 Khoj is running at {url}")
+ try:
+ webbrowser.open(url)
+ except:
+ logger.warning("🚧 Unable to open browser. Please open it manually to configure Khoj.")
+
# Show Main Window on First Run Experience or if on Linux
if args.config is None or system() not in ["Windows", "Darwin"]:
main_window.show()
diff --git a/src/khoj/processor/github/github_to_jsonl.py b/src/khoj/processor/github/github_to_jsonl.py
index 7e0de284..537f9878 100644
--- a/src/khoj/processor/github/github_to_jsonl.py
+++ b/src/khoj/processor/github/github_to_jsonl.py
@@ -39,17 +39,13 @@ class GithubToJsonl(TextToJsonl):
return
def process(self, previous_entries=None):
- # If demo mode is enabled, don't re-process any of the repositories. This is resource intensive.
- if state.demo and previous_entries is not None:
- return self.update_entries_with_ids(previous_entries, previous_entries)
-
current_entries = []
for repo in self.config.repos:
- current_entries += self.process_repo(repo, previous_entries)
+ current_entries += self.process_repo(repo)
return self.update_entries_with_ids(current_entries, previous_entries)
- def process_repo(self, repo: GithubRepoConfig, previous_entries=None):
+ def process_repo(self, repo: GithubRepoConfig):
repo_url = f"https://api.github.com/repos/{repo.owner}/{repo.name}"
repo_shorthand = f"{repo.owner}/{repo.name}"
logger.info(f"Processing github repo {repo_shorthand}")
diff --git a/src/khoj/utils/cli.py b/src/khoj/utils/cli.py
index ba0ef971..535e664b 100644
--- a/src/khoj/utils/cli.py
+++ b/src/khoj/utils/cli.py
@@ -16,9 +16,7 @@ def cli(args=None):
parser.add_argument(
"--config-file", "-c", default="~/.khoj/khoj.yml", type=pathlib.Path, help="YAML file to configure Khoj"
)
- parser.add_argument(
- "--no-gui", action="store_true", default=False, help="Do not show native desktop GUI. Default: false"
- )
+ parser.add_argument("--gui", action="store_true", default=False, help="Show native desktop GUI. Default: false")
parser.add_argument(
"--regenerate",
action="store_true",
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 98b2353c..fa9a991e 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -16,7 +16,7 @@ def test_cli_minimal_default():
# Assert
assert actual_args.config_file == resolve_absolute_path(Path("~/.khoj/khoj.yml"))
assert actual_args.regenerate == False
- assert actual_args.no_gui == False
+ assert actual_args.gui == False
assert actual_args.verbose == 0
@@ -36,11 +36,11 @@ def test_cli_invalid_config_file_path():
# ----------------------------------------------------------------------------------------------------
def test_cli_config_from_file():
# Act
- actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "--no-gui", "-vvv"])
+ actual_args = cli(["-c=tests/data/config.yml", "--regenerate", "--gui", "-vvv"])
# Assert
assert actual_args.config_file == resolve_absolute_path(Path("tests/data/config.yml"))
- assert actual_args.no_gui == True
+ assert actual_args.gui == True
assert actual_args.regenerate == True
assert actual_args.config is not None
assert actual_args.verbose == 3