diff --git a/app/ui.py b/app/ui.py index c10ea3b..2121ee2 100644 --- a/app/ui.py +++ b/app/ui.py @@ -1,11 +1,10 @@ import threading -import tkinter as tk import webbrowser from multiprocessing import Queue from pathlib import Path -from tkinter import ttk import speech_recognition as sr +import ttkbootstrap as ttk from PIL import Image, ImageTk from llm import DEFAULT_MODEL_NAME @@ -27,7 +26,7 @@ def run(self) -> None: def display_current_status(self, text: str): self.main_window.update_message(text) - class AdvancedSettingsWindow(tk.Toplevel): + class AdvancedSettingsWindow(ttk.Toplevel): """ Self-contained settings sub-window for the UI """ @@ -36,8 +35,8 @@ def __init__(self, parent): super().__init__(parent) self.title('Advanced Settings') self.minsize(300, 300) - self.create_widgets() self.settings = Settings() + self.create_widgets() # Populate UI settings_dict = self.settings.get_dict() @@ -53,8 +52,8 @@ def __init__(self, parent): def create_widgets(self) -> None: # Radio buttons for model selection - tk.Label(self, text='Select Model:').pack(pady=10, padx=10) - self.model_var = tk.StringVar(value='custom') # default selection + ttk.Label(self, text='Select Model:', bootstyle="primary").pack(pady=10, padx=10) + self.model_var = ttk.StringVar(value='custom') # default selection # Create a frame to hold the radio buttons radio_frame = ttk.Frame(self) @@ -67,9 +66,10 @@ def create_widgets(self) -> None: ('Custom (Specify Settings Below)', 'custom') ] for text, value in models: - ttk.Radiobutton(radio_frame, text=text, value=value, variable=self.model_var).pack(anchor=tk.W) + ttk.Radiobutton(radio_frame, text=text, value=value, variable=self.model_var, bootstyle="info").pack( + anchor=ttk.W) - label_base_url = tk.Label(self, text='Custom OpenAI-Like API Model Base URL') + label_base_url = ttk.Label(self, text='Custom OpenAI-Like API Model Base URL', bootstyle="secondary") label_base_url.pack(pady=10) # Entry for Base URL @@ -77,7 +77,7 @@ def create_widgets(self) -> None: self.base_url_entry.pack() # Model Label - label_model = tk.Label(self, text='Custom Model Name:') + label_model = ttk.Label(self, text='Custom Model Name:', bootstyle="secondary") label_model.pack(pady=10) # Entry for Model @@ -85,7 +85,7 @@ def create_widgets(self) -> None: self.model_entry.pack() # Save Button - save_button = ttk.Button(self, text='Save Settings', command=self.save_button) + save_button = ttk.Button(self, text='Save Settings', bootstyle="success", command=self.save_button) save_button.pack(pady=20) def save_button(self) -> None: @@ -99,7 +99,7 @@ def save_button(self) -> None: self.settings.save_settings_to_file(settings_dict) self.destroy() - class SettingsWindow(tk.Toplevel): + class SettingsWindow(ttk.Toplevel): """ Self-contained settings sub-window for the UI """ @@ -108,6 +108,7 @@ def __init__(self, parent): super().__init__(parent) self.title('Settings') self.minsize(300, 450) + self.available_themes = ['darkly', 'cyborg', 'journal', 'solar', 'superhero'] self.create_widgets() self.settings = Settings() @@ -121,76 +122,96 @@ def __init__(self, parent): self.browser_combobox.set(settings_dict['default_browser']) if 'play_ding_on_completion' in settings_dict: self.play_ding.set(1 if settings_dict['play_ding_on_completion'] else 0) - if 'custom_llm_instructions' in settings_dict: + if 'custom_llm_instructions': self.llm_instructions_text.insert('1.0', settings_dict['custom_llm_instructions']) + self.theme_combobox.set(settings_dict.get('theme', 'superhero')) def create_widgets(self) -> None: - # Label for API Key - label_api = tk.Label(self, text='OpenAI API Key:') + # API Key Widgets + label_api = ttk.Label(self, text='OpenAI API Key:', bootstyle="info") label_api.pack(pady=10) - - # Entry for API Key self.api_key_entry = ttk.Entry(self, width=30) self.api_key_entry.pack() # Label for Browser Choice - label_browser = tk.Label(self, text='Choose Default Browser:') + label_browser = ttk.Label(self, text='Choose Default Browser:', bootstyle="info") label_browser.pack(pady=10) # Dropdown for Browser Choice - self.browser_var = tk.StringVar() + self.browser_var = ttk.StringVar() self.browser_combobox = ttk.Combobox(self, textvariable=self.browser_var, values=['Safari', 'Firefox', 'Chrome']) self.browser_combobox.pack(pady=5) self.browser_combobox.set('Choose Browser') # Label for Custom LLM Instructions - label_llm = tk.Label(self, text='Custom LLM Instructions:') + label_llm = ttk.Label(self, text='Custom LLM Instructions:', bootstyle="info") label_llm.pack(pady=10) # Text Box for Custom LLM Instructions - self.llm_instructions_text = tk.Text(self, height=5, width=40) - self.llm_instructions_text.pack(pady=5) + self.llm_instructions_text = ttk.Text(self, height=10, width=50) + self.llm_instructions_text.pack(padx=(10, 10), pady=(0, 10)) # Checkbox for "Play Ding" option - self.play_ding = tk.IntVar() - play_ding_checkbox = ttk.Checkbutton(self, text="Play Ding on Completion", variable=self.play_ding) + self.play_ding = ttk.IntVar() + play_ding_checkbox = ttk.Checkbutton(self, text="Play Ding on Completion", variable=self.play_ding, + bootstyle="round-toggle") play_ding_checkbox.pack(pady=10) + # Theme Selection Widgets + label_theme = ttk.Label(self, text='UI Theme:', bootstyle="info") + label_theme.pack() + self.theme_var = ttk.StringVar() + self.theme_combobox = ttk.Combobox(self, textvariable=self.theme_var, values=self.available_themes, + state="readonly") + self.theme_combobox.pack(pady=5) + self.theme_combobox.set('superhero') + # Add binding for immediate theme change + self.theme_combobox.bind('<>', self.on_theme_change) + # Save Button - save_button = ttk.Button(self, text='Save Settings', command=self.save_button) - save_button.pack(pady=(10, 0)) + save_button = ttk.Button(self, text='Save Settings', bootstyle="success", command=self.save_button) + save_button.pack(pady=(10, 5)) # Button to open Advanced Settings - advanced_settings_button = ttk.Button(self, text='Advanced Settings', command=self.open_advanced_settings) + advanced_settings_button = ttk.Button(self, text='Advanced Settings', bootstyle="info", + command=self.open_advanced_settings) advanced_settings_button.pack(pady=(0, 10)) # Hyperlink Label - link_label = tk.Label(self, text='Instructions', fg='#499CE4') + link_label = ttk.Label(self, text='Instructions', bootstyle="primary") link_label.pack() link_label.bind('', lambda e: open_link( 'https://github.com/AmberSahdev/Open-Interface?tab=readme-ov-file#setup-%EF%B8%8F')) # Check for updates Label - update_label = tk.Label(self, text='Check for Updates', fg='#499CE4', font=('Helvetica', 10)) + update_label = ttk.Label(self, text='Check for Updates', bootstyle="primary") update_label.pack() update_label.bind('', lambda e: open_link( 'https://github.com/AmberSahdev/Open-Interface/releases/latest')) # Version Label - version_label = tk.Label(self, text=f'Version: {str(version)}', font=('Helvetica', 10)) + version_label = ttk.Label(self, text=f'Version: {str(version)}', font=('Helvetica', 10)) version_label.pack(side="bottom", pady=10) + def on_theme_change(self, event=None) -> None: + # Apply theme immediately when selected + theme = self.theme_var.get() + self.master.change_theme(theme) + def save_button(self) -> None: + theme = self.theme_var.get() api_key = self.api_key_entry.get().strip() default_browser = self.browser_var.get() settings_dict = { 'api_key': api_key, 'default_browser': default_browser, 'play_ding_on_completion': bool(self.play_ding.get()), - 'custom_llm_instructions': self.llm_instructions_text.get("1.0", "end-1c").strip() + 'custom_llm_instructions': self.llm_instructions_text.get("1.0", "end-1c").strip(), + 'theme': theme } + # Remove redundant theme change since it's already applied self.settings.save_settings_to_file(settings_dict) self.destroy() @@ -198,11 +219,27 @@ def open_advanced_settings(self): # Open the advanced settings window UI.AdvancedSettingsWindow(self) - class MainWindow(tk.Tk): + class MainWindow(ttk.Window): + def change_theme(self, theme_name: str) -> None: + self.style.theme_use(theme_name) + def __init__(self): - super().__init__() + settings = Settings() + settings_dict = settings.get_dict() + theme = settings_dict.get('theme', 'superhero') + + super().__init__(themename=theme) self.title('Open Interface') - self.minsize(420, 250) + window_width = 420 + window_height = 250 + self.minsize(window_width, window_height) + + # Set the geometry of the window + # Calculate position for bottom right corner + screen_width = self.winfo_screenwidth() + x_position = screen_width - window_width - 10 # 10px margin from the right edge + y_position = 50 # 50px margin from the bottom edge + self.geometry(f'{window_width}x{window_height}+{x_position}+{y_position}') # PhotoImage object needs to persist as long as the app does, hence it's a class object. path_to_icon_png = Path(__file__).resolve().parent.joinpath('resources', 'icon.png') @@ -224,45 +261,49 @@ def create_widgets(self) -> None: # Creates and arranges the UI elements # Frame frame = ttk.Frame(self, padding='10 10 10 10') - frame.grid(column=0, row=0, sticky=(tk.W, tk.E, tk.N, tk.S)) + frame.grid(column=0, row=0, sticky=(ttk.W, ttk.E, ttk.N, ttk.S)) frame.columnconfigure(0, weight=1) - logo_label = tk.Label(frame, image=self.logo_img) - logo_label.grid(column=0, row=0, sticky=tk.W, pady=(10, 20)) + logo_label = ttk.Label(frame, image=self.logo_img) + logo_label.grid(column=0, row=0, sticky=ttk.W, pady=(10, 20)) # Heading Label - heading_label = tk.Label(frame, text='What would you like me to do?', font=('Helvetica', 16), - wraplength=300) - heading_label.grid(column=0, row=1, columnspan=3, sticky=tk.W) + heading_label = ttk.Label(frame, text='What would you like me to do?', font=('Helvetica', 16), + bootstyle="primary", + wraplength=300) + heading_label.grid(column=0, row=1, columnspan=3, sticky=ttk.W) # Entry widget self.entry = ttk.Entry(frame, width=30) - self.entry.grid(column=0, row=2, sticky=(tk.W, tk.E)) + self.entry.grid(column=0, row=2, sticky=(ttk.W, ttk.E)) + + # Bind the Enter key to the submit function + self.entry.bind("", lambda event: self.execute_user_request()) + self.entry.bind("", lambda event: self.execute_user_request()) # Submit Button - button = ttk.Button(frame, text='Submit', command=self.execute_user_request) + button = ttk.Button(frame, text='Submit', bootstyle="success", command=self.execute_user_request) button.grid(column=2, row=2) # Mic Button - mic_button = tk.Button(frame, image=self.mic_icon, command=self.start_voice_input_thread, borderwidth=0, - highlightthickness=0) + mic_button = ttk.Button(frame, image=self.mic_icon, bootstyle="link", command=self.start_voice_input_thread) mic_button.grid(column=1, row=2, padx=(0, 5)) # Settings Button - settings_button = ttk.Button(self, text='Settings', command=self.open_settings) + settings_button = ttk.Button(self, text='Settings', bootstyle="info-outline", command=self.open_settings) settings_button.place(relx=1.0, rely=0.0, anchor='ne', x=-5, y=5) # Stop Button - stop_button = ttk.Button(self, text='Stop', command=self.stop_previous_request) + stop_button = ttk.Button(self, text='Stop', bootstyle="danger-outline", command=self.stop_previous_request) stop_button.place(relx=1.0, rely=1.0, anchor='se', x=-10, y=-10) # Text display for echoed input - self.input_display = tk.Label(frame, text='', font=('Helvetica', 16), wraplength=400) - self.input_display.grid(column=0, row=3, columnspan=3, sticky=tk.W) + self.input_display = ttk.Label(frame, text='', font=('Helvetica', 16), wraplength=400) + self.input_display.grid(column=0, row=3, columnspan=3, sticky=ttk.W) # Text display for additional messages - self.message_display = tk.Label(frame, text='', font=('Helvetica', 14), wraplength=400) - self.message_display.grid(column=0, row=6, columnspan=3, sticky=tk.W) + self.message_display = ttk.Label(frame, text='', font=('Helvetica', 14), wraplength=400) + self.message_display.grid(column=0, row=6, columnspan=3, sticky=ttk.W) def open_settings(self) -> None: UI.SettingsWindow(self) @@ -277,7 +318,7 @@ def display_input(self) -> str: self.input_display['text'] = f'{user_input}' # Clear the entry widget - self.entry.delete(0, tk.END) + self.entry.delete(0, ttk.END) return user_input.strip() @@ -307,7 +348,7 @@ def voice_input(self) -> None: audio = recognizer.listen(source, timeout=4) try: text = recognizer.recognize_google(audio) - self.entry.delete(0, tk.END) + self.entry.delete(0, ttk.END) self.entry.insert(0, text) self.update_message('') except sr.UnknownValueError: