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:
Debanjum 2022-08-13 00:06:46 +03:00 committed by GitHub
commit b85a626fc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 112 additions and 47 deletions

View file

@ -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)

View file

@ -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))

View 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()

View file

@ -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