mirror of
https://github.com/khoj-ai/khoj.git
synced 2024-11-24 07:55:07 +01:00
Improve Configure Screen on Desktop Interface
- Reload settings in separate thread to not freeze Config Screen - Allow user to quit application from the terminal via SIGINT - Update Title of Configure Screen to follow "[Screen] - App" pattern - Increase Width of Configure Screen - Let Input Fields Wrap. Adjust Height based on Text in Field - Rename FileBrowser Button Text to "Select" instead of "Add" - Reduce user confusion on how to add multiple files per content type - Create a reusable class for the Conversation Label+Text Input Field - Deep copy default_config when assigning to prevent it being overwritten by updates
This commit is contained in:
commit
b85a626fc9
4 changed files with 112 additions and 47 deletions
|
@ -1,13 +1,16 @@
|
||||||
# Standard Packages
|
# Standard Packages
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
|
||||||
# External Packages
|
# External Packages
|
||||||
from PyQt6 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt, QThread, QObject, pyqtSignal
|
||||||
|
|
||||||
# Internal Packages
|
# Internal Packages
|
||||||
from src.configure import configure_server
|
from src.configure import configure_server
|
||||||
from src.interface.desktop.file_browser import FileBrowser
|
from src.interface.desktop.file_browser import FileBrowser
|
||||||
|
from src.interface.desktop.labelled_text_field import LabelledTextField
|
||||||
from src.utils import constants, state, yaml as yaml_utils
|
from src.utils import constants, state, yaml as yaml_utils
|
||||||
from src.utils.cli import cli
|
from src.utils.cli import cli
|
||||||
from src.utils.config import SearchType, ProcessorType
|
from src.utils.config import SearchType, ProcessorType
|
||||||
|
@ -30,12 +33,13 @@ class ConfigureScreen(QtWidgets.QDialog):
|
||||||
if resolve_absolute_path(self.config_file).exists():
|
if resolve_absolute_path(self.config_file).exists():
|
||||||
self.current_config = yaml_utils.load_config_from_file(self.config_file)
|
self.current_config = yaml_utils.load_config_from_file(self.config_file)
|
||||||
else:
|
else:
|
||||||
self.current_config = constants.default_config
|
self.current_config = deepcopy(constants.default_config)
|
||||||
self.new_config = self.current_config
|
self.new_config = self.current_config
|
||||||
|
|
||||||
# Initialize Configure Window
|
# Initialize Configure Window
|
||||||
self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
|
self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint)
|
||||||
self.setWindowTitle("Khoj - Configure")
|
self.setWindowTitle("Configure - Khoj")
|
||||||
|
self.setFixedWidth(600)
|
||||||
|
|
||||||
# Initialize Configure Window Layout
|
# Initialize Configure Window Layout
|
||||||
layout = QtWidgets.QVBoxLayout()
|
layout = QtWidgets.QVBoxLayout()
|
||||||
|
@ -86,32 +90,26 @@ class ConfigureScreen(QtWidgets.QDialog):
|
||||||
|
|
||||||
def add_processor_panel(self, current_conversation_config: dict, processor_type: ProcessorType, parent_layout: QtWidgets.QLayout):
|
def add_processor_panel(self, current_conversation_config: dict, processor_type: ProcessorType, parent_layout: QtWidgets.QLayout):
|
||||||
"Add Conversation Processor Panel"
|
"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)
|
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_settings = QtWidgets.QWidget()
|
||||||
processor_type_layout = QtWidgets.QVBoxLayout(processor_type_settings)
|
processor_type_layout = QtWidgets.QVBoxLayout(processor_type_settings)
|
||||||
|
|
||||||
enable_conversation = ProcessorCheckBox(f"Conversation", processor_type)
|
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, current_openai_api_key)
|
||||||
|
|
||||||
|
# Set enabled/disabled based on checkbox state
|
||||||
enable_conversation.setChecked(current_openai_api_key is not None)
|
enable_conversation.setChecked(current_openai_api_key is not None)
|
||||||
|
|
||||||
conversation_settings = QtWidgets.QWidget()
|
|
||||||
conversation_settings_layout = QtWidgets.QHBoxLayout(conversation_settings)
|
|
||||||
input_label = QtWidgets.QLabel()
|
|
||||||
input_label.setText("OpenAI API Key")
|
|
||||||
input_label.setFixedWidth(95)
|
|
||||||
|
|
||||||
input_field = ProcessorLineEdit(current_openai_api_key, processor_type)
|
|
||||||
input_field.setFixedWidth(245)
|
|
||||||
|
|
||||||
input_field.setEnabled(enable_conversation.isChecked())
|
input_field.setEnabled(enable_conversation.isChecked())
|
||||||
enable_conversation.stateChanged.connect(lambda _: input_field.setEnabled(enable_conversation.isChecked()))
|
enable_conversation.stateChanged.connect(lambda _: input_field.setEnabled(enable_conversation.isChecked()))
|
||||||
|
|
||||||
conversation_settings_layout.addWidget(input_label)
|
# Add setting widgets for given processor type to panel
|
||||||
conversation_settings_layout.addWidget(input_field)
|
|
||||||
|
|
||||||
processor_type_layout.addWidget(enable_conversation)
|
processor_type_layout.addWidget(enable_conversation)
|
||||||
processor_type_layout.addWidget(conversation_settings)
|
processor_type_layout.addWidget(input_field)
|
||||||
|
|
||||||
parent_layout.addWidget(processor_type_settings)
|
parent_layout.addWidget(processor_type_settings)
|
||||||
|
|
||||||
return processor_type_settings
|
return processor_type_settings
|
||||||
|
|
||||||
def add_action_panel(self, parent_layout: QtWidgets.QLayout):
|
def add_action_panel(self, parent_layout: QtWidgets.QLayout):
|
||||||
|
@ -120,9 +118,9 @@ class ConfigureScreen(QtWidgets.QDialog):
|
||||||
action_bar = QtWidgets.QWidget()
|
action_bar = QtWidgets.QWidget()
|
||||||
action_bar_layout = QtWidgets.QHBoxLayout(action_bar)
|
action_bar_layout = QtWidgets.QHBoxLayout(action_bar)
|
||||||
|
|
||||||
save_button = QtWidgets.QPushButton("Start", clicked=self.save_settings)
|
self.save_button = QtWidgets.QPushButton("Start", clicked=self.save_settings)
|
||||||
|
|
||||||
action_bar_layout.addWidget(save_button)
|
action_bar_layout.addWidget(self.save_button)
|
||||||
parent_layout.addWidget(action_bar)
|
parent_layout.addWidget(action_bar)
|
||||||
|
|
||||||
def get_default_config(self, search_type:SearchType=None, processor_type:ProcessorType=None):
|
def get_default_config(self, search_type:SearchType=None, processor_type:ProcessorType=None):
|
||||||
|
@ -165,9 +163,7 @@ class ConfigureScreen(QtWidgets.QDialog):
|
||||||
"Update config with conversation settings from UI"
|
"Update config with conversation settings from UI"
|
||||||
for settings_panel in self.processor_settings_panels:
|
for settings_panel in self.processor_settings_panels:
|
||||||
for child in settings_panel.children():
|
for child in settings_panel.children():
|
||||||
if isinstance(child, QtWidgets.QWidget) and child.findChild(ProcessorLineEdit):
|
if not isinstance(child, (ProcessorCheckBox, LabelledTextField)):
|
||||||
child = child.findChild(ProcessorLineEdit)
|
|
||||||
elif not isinstance(child, ProcessorCheckBox):
|
|
||||||
continue
|
continue
|
||||||
if isinstance(child, ProcessorCheckBox):
|
if isinstance(child, ProcessorCheckBox):
|
||||||
# Processor Type Disabled
|
# Processor Type Disabled
|
||||||
|
@ -178,9 +174,9 @@ class ConfigureScreen(QtWidgets.QDialog):
|
||||||
current_processor_config = self.current_config['processor'].get(child.processor_type, {})
|
current_processor_config = self.current_config['processor'].get(child.processor_type, {})
|
||||||
default_processor_config = self.get_default_config(processor_type = 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)
|
self.new_config['processor'][child.processor_type.value] = merge_dicts(current_processor_config, default_processor_config)
|
||||||
elif isinstance(child, ProcessorLineEdit) and child.processor_type in self.new_config['processor']:
|
elif isinstance(child, LabelledTextField) and child.processor_type in self.new_config['processor']:
|
||||||
if child.processor_type == ProcessorType.Conversation:
|
if child.processor_type == ProcessorType.Conversation:
|
||||||
self.new_config['processor'][child.processor_type.value]['openai-api-key'] = child.text() if child.text() != '' else None
|
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:
|
def save_settings_to_file(self) -> bool:
|
||||||
# Validate config before writing to file
|
# Validate config before writing to file
|
||||||
|
@ -212,12 +208,45 @@ class ConfigureScreen(QtWidgets.QDialog):
|
||||||
configure_server(args, required=True)
|
configure_server(args, required=True)
|
||||||
|
|
||||||
def save_settings(self):
|
def save_settings(self):
|
||||||
"Save the settings to khoj.yml"
|
"Save the new settings to khoj.yml. Reload app with updated settings"
|
||||||
self.update_search_settings()
|
self.update_search_settings()
|
||||||
self.update_processor_settings()
|
self.update_processor_settings()
|
||||||
if self.save_settings_to_file():
|
if self.save_settings_to_file():
|
||||||
self.load_updated_settings()
|
# Setup thread
|
||||||
self.hide()
|
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.thread.finished.connect(self.thread.deleteLater)
|
||||||
|
|
||||||
|
# Start thread
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
# Disable Save Button
|
||||||
|
self.save_button.setEnabled(False)
|
||||||
|
self.save_button.setText("Saving...")
|
||||||
|
|
||||||
|
# Reset UI
|
||||||
|
self.thread.finished.connect(lambda: self.save_button.setText("Start"))
|
||||||
|
self.thread.finished.connect(lambda: self.save_button.setEnabled(True))
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsLoader(QObject):
|
||||||
|
"Load Settings Thread"
|
||||||
|
finished = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, load_settings_func):
|
||||||
|
super(SettingsLoader, self).__init__()
|
||||||
|
self.load_settings_func = load_settings_func
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"Load Settings"
|
||||||
|
self.load_settings_func()
|
||||||
|
self.finished.emit()
|
||||||
|
|
||||||
|
|
||||||
class SearchCheckBox(QtWidgets.QCheckBox):
|
class SearchCheckBox(QtWidgets.QCheckBox):
|
||||||
|
@ -230,12 +259,3 @@ class ProcessorCheckBox(QtWidgets.QCheckBox):
|
||||||
def __init__(self, text, processor_type: ProcessorType, parent=None):
|
def __init__(self, text, processor_type: ProcessorType, parent=None):
|
||||||
self.processor_type = processor_type
|
self.processor_type = processor_type
|
||||||
super(ProcessorCheckBox, self).__init__(text, parent=parent)
|
super(ProcessorCheckBox, self).__init__(text, parent=parent)
|
||||||
|
|
||||||
|
|
||||||
class ProcessorLineEdit(QtWidgets.QLineEdit):
|
|
||||||
def __init__(self, text, processor_type: ProcessorType, parent=None):
|
|
||||||
self.processor_type = processor_type
|
|
||||||
if text is None:
|
|
||||||
super(ProcessorLineEdit, self).__init__(parent=parent)
|
|
||||||
else:
|
|
||||||
super(ProcessorLineEdit, self).__init__(text, parent=parent)
|
|
||||||
|
|
|
@ -19,15 +19,17 @@ class FileBrowser(QtWidgets.QWidget):
|
||||||
self.label = QtWidgets.QLabel()
|
self.label = QtWidgets.QLabel()
|
||||||
self.label.setText(title)
|
self.label.setText(title)
|
||||||
self.label.setFixedWidth(95)
|
self.label.setFixedWidth(95)
|
||||||
|
self.label.setWordWrap(True)
|
||||||
layout.addWidget(self.label)
|
layout.addWidget(self.label)
|
||||||
|
|
||||||
self.lineEdit = QtWidgets.QLineEdit(self)
|
self.lineEdit = QtWidgets.QPlainTextEdit(self)
|
||||||
self.lineEdit.setFixedWidth(180)
|
self.lineEdit.setFixedWidth(330)
|
||||||
self.setFiles(default_files)
|
self.setFiles(default_files)
|
||||||
|
self.lineEdit.setFixedHeight(min(7+20*len(self.lineEdit.toPlainText().split('\n')),90))
|
||||||
|
self.lineEdit.textChanged.connect(self.updateFieldHeight)
|
||||||
layout.addWidget(self.lineEdit)
|
layout.addWidget(self.lineEdit)
|
||||||
|
|
||||||
self.button = QtWidgets.QPushButton('Add')
|
self.button = QtWidgets.QPushButton('Select')
|
||||||
self.button.clicked.connect(self.storeFilesSelectedInFileDialog)
|
self.button.clicked.connect(self.storeFilesSelectedInFileDialog)
|
||||||
layout.addWidget(self.button)
|
layout.addWidget(self.button)
|
||||||
layout.addStretch()
|
layout.addStretch()
|
||||||
|
@ -60,12 +62,15 @@ class FileBrowser(QtWidgets.QWidget):
|
||||||
if not self.filepaths or len(self.filepaths) == 0:
|
if not self.filepaths or len(self.filepaths) == 0:
|
||||||
return
|
return
|
||||||
elif len(self.filepaths) == 1:
|
elif len(self.filepaths) == 1:
|
||||||
self.lineEdit.setText(self.filepaths[0])
|
self.lineEdit.setPlainText(self.filepaths[0])
|
||||||
else:
|
else:
|
||||||
self.lineEdit.setText(",".join(self.filepaths))
|
self.lineEdit.setPlainText("\n".join(self.filepaths))
|
||||||
|
|
||||||
def getPaths(self):
|
def getPaths(self):
|
||||||
if self.lineEdit.text() == '':
|
if self.lineEdit.toPlainText() == '':
|
||||||
return []
|
return []
|
||||||
else:
|
else:
|
||||||
return self.lineEdit.text().split(',')
|
return self.lineEdit.toPlainText().split('\n')
|
||||||
|
|
||||||
|
def updateFieldHeight(self):
|
||||||
|
self.lineEdit.setFixedHeight(min(7+20*len(self.lineEdit.toPlainText().split('\n')),90))
|
||||||
|
|
27
src/interface/desktop/labelled_text_field.py
Normal file
27
src/interface/desktop/labelled_text_field.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# External Packages
|
||||||
|
from PyQt6 import QtWidgets
|
||||||
|
|
||||||
|
# Internal Packages
|
||||||
|
from src.utils.config import ProcessorType
|
||||||
|
|
||||||
|
|
||||||
|
class LabelledTextField(QtWidgets.QWidget):
|
||||||
|
def __init__(self, title, processor_type: ProcessorType=None, default_value: str=None):
|
||||||
|
QtWidgets.QWidget.__init__(self)
|
||||||
|
layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.setLayout(layout)
|
||||||
|
self.processor_type = processor_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()
|
15
src/main.py
15
src/main.py
|
@ -1,4 +1,5 @@
|
||||||
# Standard Packages
|
# Standard Packages
|
||||||
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# External Packages
|
# External Packages
|
||||||
|
@ -6,7 +7,7 @@ import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from PyQt6 import QtWidgets
|
from PyQt6 import QtWidgets
|
||||||
from PyQt6.QtCore import QThread
|
from PyQt6.QtCore import QThread, QTimer
|
||||||
|
|
||||||
# Internal Packages
|
# Internal Packages
|
||||||
from src.configure import configure_server
|
from src.configure import configure_server
|
||||||
|
@ -49,12 +50,24 @@ def run():
|
||||||
if args.config is None:
|
if args.config is None:
|
||||||
configure_screen.show()
|
configure_screen.show()
|
||||||
|
|
||||||
|
# Setup Signal Handlers
|
||||||
|
signal.signal(signal.SIGINT, sigint_handler)
|
||||||
|
# Invoke python Interpreter every 500ms to handle signals
|
||||||
|
timer = QTimer()
|
||||||
|
timer.start(500)
|
||||||
|
timer.timeout.connect(lambda: None)
|
||||||
|
|
||||||
# Start Application
|
# Start Application
|
||||||
server.start()
|
server.start()
|
||||||
gui.aboutToQuit.connect(server.terminate)
|
gui.aboutToQuit.connect(server.terminate)
|
||||||
gui.exec()
|
gui.exec()
|
||||||
|
|
||||||
|
|
||||||
|
def sigint_handler(*args):
|
||||||
|
print("\nShutting down Khoj...")
|
||||||
|
QtWidgets.QApplication.quit()
|
||||||
|
|
||||||
|
|
||||||
def set_state(args):
|
def set_state(args):
|
||||||
state.config_file = args.config_file
|
state.config_file = args.config_file
|
||||||
state.config = args.config
|
state.config = args.config
|
||||||
|
|
Loading…
Reference in a new issue