first commit
This commit is contained in:
867
settings_manager.py
Normal file
867
settings_manager.py
Normal file
@@ -0,0 +1,867 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox, filedialog
|
||||
|
||||
|
||||
class SettingsManager:
|
||||
"""Manages application settings with JSON persistence."""
|
||||
|
||||
def __init__(self, settings_file: str = "settings.json"):
|
||||
self.settings_file = settings_file
|
||||
self.default_settings = {
|
||||
"api": {
|
||||
"huggingface_token": "",
|
||||
"use_env_token": True,
|
||||
"use_organization": False,
|
||||
"organization": ""
|
||||
},
|
||||
"paths": {
|
||||
"models_directory": "./models",
|
||||
"downloads_directory": "./downloads",
|
||||
"last_model_path": "",
|
||||
"last_lora_path": ""
|
||||
},
|
||||
"model_settings": {
|
||||
"default_n_ctx": 4096,
|
||||
"default_n_gpu_layers": 0,
|
||||
"default_max_tokens": 256,
|
||||
"stream_by_default": True,
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
"repetition_penalty": 1.1,
|
||||
"no_repeat_ngram_size": 0,
|
||||
"min_p": 0.0,
|
||||
"typical_p": 1.0
|
||||
},
|
||||
"search_preferences": {
|
||||
"default_search_type": "Models",
|
||||
"default_sort": "downloads",
|
||||
"search_limit": 50,
|
||||
"auto_filter_gguf": True
|
||||
},
|
||||
"ui_preferences": {
|
||||
"window_width": 1200,
|
||||
"window_height": 700,
|
||||
"theme": "default",
|
||||
"show_tooltips": True
|
||||
},
|
||||
"library": {
|
||||
"root_folder": "",
|
||||
"max_depth": 3,
|
||||
"auto_scan_on_startup": False,
|
||||
"watch_for_changes": False
|
||||
},
|
||||
"download_settings": {
|
||||
"max_concurrent_downloads": 3,
|
||||
"max_download_speed": 0,
|
||||
"min_download_speed": 0,
|
||||
"retry_attempts": 3,
|
||||
"timeout_seconds": 30
|
||||
}
|
||||
}
|
||||
self.settings = self.load_settings()
|
||||
|
||||
def load_settings(self) -> Dict[str, Any]:
|
||||
"""Load settings from file or create default."""
|
||||
if os.path.exists(self.settings_file):
|
||||
try:
|
||||
with open(self.settings_file, 'r') as f:
|
||||
loaded = json.load(f)
|
||||
# Merge with defaults to handle new keys
|
||||
return self._merge_settings(self.default_settings, loaded)
|
||||
except Exception as e:
|
||||
print(f"Error loading settings: {e}")
|
||||
return self.default_settings.copy()
|
||||
return self.default_settings.copy()
|
||||
|
||||
def _merge_settings(self, defaults: Dict, loaded: Dict) -> Dict:
|
||||
"""Merge loaded settings with defaults, preserving user values."""
|
||||
result = defaults.copy()
|
||||
for key, value in loaded.items():
|
||||
if key in result:
|
||||
if isinstance(result[key], dict) and isinstance(value, dict):
|
||||
result[key] = self._merge_settings(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def save_settings(self):
|
||||
"""Save current settings to file."""
|
||||
try:
|
||||
with open(self.settings_file, 'w') as f:
|
||||
json.dump(self.settings, f, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving settings: {e}")
|
||||
return False
|
||||
|
||||
def get(self, path: str, default: Any = None) -> Any:
|
||||
"""Get a setting value using dot notation (e.g., 'api.huggingface_token')."""
|
||||
keys = path.split('.')
|
||||
value = self.settings
|
||||
for key in keys:
|
||||
if isinstance(value, dict) and key in value:
|
||||
value = value[key]
|
||||
else:
|
||||
return default
|
||||
return value
|
||||
|
||||
def set(self, path: str, value: Any):
|
||||
"""Set a setting value using dot notation."""
|
||||
keys = path.split('.')
|
||||
target = self.settings
|
||||
for key in keys[:-1]:
|
||||
if key not in target:
|
||||
target[key] = {}
|
||||
target = target[key]
|
||||
target[keys[-1]] = value
|
||||
|
||||
def reset_to_defaults(self):
|
||||
"""Reset all settings to defaults."""
|
||||
self.settings = self.default_settings.copy()
|
||||
self.save_settings()
|
||||
|
||||
|
||||
class SettingsDialog:
|
||||
"""Settings dialog window for the application."""
|
||||
|
||||
def __init__(self, parent: tk.Tk, settings_manager: SettingsManager):
|
||||
self.parent = parent
|
||||
self.settings = settings_manager
|
||||
self.dialog = tk.Toplevel(parent)
|
||||
self.dialog.title("Settings")
|
||||
self.dialog.geometry("700x500")
|
||||
self.dialog.resizable(True, True)
|
||||
|
||||
# Make dialog modal
|
||||
self.dialog.transient(parent)
|
||||
self.dialog.grab_set()
|
||||
|
||||
# Variables for settings
|
||||
self.vars = {}
|
||||
self._create_variables()
|
||||
|
||||
# Build UI
|
||||
self._build_ui()
|
||||
|
||||
# Load current settings into UI
|
||||
self._load_current_settings()
|
||||
|
||||
# Center the dialog
|
||||
self._center_window()
|
||||
|
||||
def _create_variables(self):
|
||||
"""Create tkinter variables for settings."""
|
||||
self.vars = {
|
||||
# API Settings
|
||||
'hf_token': tk.StringVar(),
|
||||
'use_env_token': tk.BooleanVar(),
|
||||
'use_organization': tk.BooleanVar(),
|
||||
'organization': tk.StringVar(),
|
||||
|
||||
# Path Settings
|
||||
'models_dir': tk.StringVar(),
|
||||
'downloads_dir': tk.StringVar(),
|
||||
|
||||
# Model Settings
|
||||
'default_n_ctx': tk.IntVar(),
|
||||
'default_n_gpu': tk.IntVar(),
|
||||
'default_max_tokens': tk.IntVar(),
|
||||
'stream_default': tk.BooleanVar(),
|
||||
'temperature': tk.DoubleVar(),
|
||||
'top_p': tk.DoubleVar(),
|
||||
'repetition_penalty': tk.DoubleVar(),
|
||||
'no_repeat_ngram_size': tk.IntVar(),
|
||||
'min_p': tk.DoubleVar(),
|
||||
'typical_p': tk.DoubleVar(),
|
||||
|
||||
# Search Settings
|
||||
'search_type': tk.StringVar(),
|
||||
'default_sort': tk.StringVar(),
|
||||
'search_limit': tk.IntVar(),
|
||||
'auto_filter_gguf': tk.BooleanVar(),
|
||||
|
||||
# UI Settings
|
||||
'show_tooltips': tk.BooleanVar(),
|
||||
'theme': tk.StringVar(),
|
||||
|
||||
# Library Settings
|
||||
'library_root': tk.StringVar(),
|
||||
'library_depth': tk.IntVar(),
|
||||
'auto_scan': tk.BooleanVar(),
|
||||
'watch_changes': tk.BooleanVar(),
|
||||
|
||||
# Download Settings
|
||||
'max_downloads': tk.IntVar(),
|
||||
'max_speed': tk.IntVar(),
|
||||
'min_speed': tk.IntVar(),
|
||||
'retry_attempts': tk.IntVar(),
|
||||
'timeout_seconds': tk.IntVar()
|
||||
}
|
||||
|
||||
def _build_ui(self):
|
||||
"""Build the settings dialog UI."""
|
||||
# Create notebook for tabs
|
||||
notebook = ttk.Notebook(self.dialog)
|
||||
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# API Settings Tab
|
||||
api_frame = ttk.Frame(notebook)
|
||||
notebook.add(api_frame, text="API")
|
||||
self._build_api_tab(api_frame)
|
||||
|
||||
# Paths Tab
|
||||
paths_frame = ttk.Frame(notebook)
|
||||
notebook.add(paths_frame, text="Paths")
|
||||
self._build_paths_tab(paths_frame)
|
||||
|
||||
# Model Settings Tab
|
||||
model_frame = ttk.Frame(notebook)
|
||||
notebook.add(model_frame, text="Model Defaults")
|
||||
self._build_model_tab(model_frame)
|
||||
|
||||
# Search Settings Tab
|
||||
search_frame = ttk.Frame(notebook)
|
||||
notebook.add(search_frame, text="Search")
|
||||
self._build_search_tab(search_frame)
|
||||
|
||||
# UI Preferences Tab
|
||||
ui_frame = ttk.Frame(notebook)
|
||||
notebook.add(ui_frame, text="Interface")
|
||||
self._build_ui_tab(ui_frame)
|
||||
|
||||
# Library Settings Tab
|
||||
library_frame = ttk.Frame(notebook)
|
||||
notebook.add(library_frame, text="Library")
|
||||
self._build_library_tab(library_frame)
|
||||
|
||||
# Download Settings Tab
|
||||
download_frame = ttk.Frame(notebook)
|
||||
notebook.add(download_frame, text="Downloads")
|
||||
self._build_download_tab(download_frame)
|
||||
|
||||
# Buttons at bottom
|
||||
button_frame = ttk.Frame(self.dialog)
|
||||
button_frame.pack(fill=tk.X, padx=10, pady=(0, 10))
|
||||
|
||||
ttk.Button(button_frame, text="Save", command=self._save_settings).pack(side=tk.RIGHT, padx=5)
|
||||
ttk.Button(button_frame, text="Cancel", command=self.dialog.destroy).pack(side=tk.RIGHT)
|
||||
ttk.Button(button_frame, text="Reset to Defaults", command=self._reset_defaults).pack(side=tk.LEFT)
|
||||
|
||||
def _build_api_tab(self, parent: ttk.Frame):
|
||||
"""Build API settings tab."""
|
||||
# Main container with scrollbar
|
||||
canvas = tk.Canvas(parent)
|
||||
scrollbar = ttk.Scrollbar(parent, orient="vertical", command=canvas.yview)
|
||||
scrollable_frame = ttk.Frame(canvas)
|
||||
|
||||
scrollable_frame.bind(
|
||||
"<Configure>",
|
||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
|
||||
)
|
||||
|
||||
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
|
||||
# API Token Frame
|
||||
token_frame = ttk.LabelFrame(scrollable_frame, text="API Token Management", padding=10)
|
||||
token_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Use environment token checkbox
|
||||
ttk.Checkbutton(token_frame, text="Use token from HUGGINGFACE.env file",
|
||||
variable=self.vars['use_env_token'],
|
||||
command=self._toggle_token_entry).grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
# API Token entry
|
||||
ttk.Label(token_frame, text="API Token:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
self.token_entry = ttk.Entry(token_frame, textvariable=self.vars['hf_token'], width=40, show="*")
|
||||
self.token_entry.grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Token action buttons
|
||||
token_buttons = ttk.Frame(token_frame)
|
||||
token_buttons.grid(row=1, column=2, padx=5)
|
||||
|
||||
self.show_token_btn = ttk.Button(token_buttons, text="View", width=8,
|
||||
command=self._toggle_token_visibility)
|
||||
self.show_token_btn.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
ttk.Button(token_buttons, text="Change", width=8,
|
||||
command=self._change_token).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
ttk.Button(token_buttons, text="Test", width=8,
|
||||
command=self._test_api_key).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Organization Frame
|
||||
org_frame = ttk.LabelFrame(scrollable_frame, text="Organization Settings", padding=10)
|
||||
org_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Use organization checkbox
|
||||
ttk.Checkbutton(org_frame, text="Use as HuggingFace organization member",
|
||||
variable=self.vars['use_organization'],
|
||||
command=self._toggle_organization).grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
# Organization dropdown
|
||||
ttk.Label(org_frame, text="Organization:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
self.org_combo = ttk.Combobox(org_frame, textvariable=self.vars['organization'],
|
||||
state="disabled", width=30)
|
||||
self.org_combo.grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Button(org_frame, text="Fetch Organizations",
|
||||
command=self._fetch_organizations).grid(row=1, column=2, padx=5)
|
||||
|
||||
# Organizations list (for display)
|
||||
self.org_listbox = tk.Listbox(org_frame, height=5, width=50)
|
||||
self.org_listbox.grid(row=2, column=0, columnspan=3, pady=10)
|
||||
self.org_listbox.bind('<<ListboxSelect>>', self._on_org_select)
|
||||
|
||||
# Info labels
|
||||
info_frame = ttk.Frame(scrollable_frame)
|
||||
info_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
info_text = ("• API tokens can be obtained from: https://huggingface.co/settings/tokens\n"
|
||||
"• Organizations allow you to access private repos and team resources\n"
|
||||
"• Test your API key to verify it's working correctly")
|
||||
ttk.Label(info_frame, text=info_text, foreground="gray").pack(anchor=tk.W)
|
||||
|
||||
# API Status label
|
||||
self.api_status_label = ttk.Label(info_frame, text="", foreground="green")
|
||||
self.api_status_label.pack(anchor=tk.W, pady=5)
|
||||
|
||||
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
def _build_paths_tab(self, parent: ttk.Frame):
|
||||
"""Build paths settings tab."""
|
||||
frame = ttk.LabelFrame(parent, text="Default Directories", padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Models directory
|
||||
ttk.Label(frame, text="Models Directory:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
ttk.Entry(frame, textvariable=self.vars['models_dir'], width=40).grid(row=0, column=1, pady=5)
|
||||
ttk.Button(frame, text="Browse",
|
||||
command=lambda: self._browse_directory('models_dir')).grid(row=0, column=2, padx=5)
|
||||
|
||||
# Downloads directory
|
||||
ttk.Label(frame, text="Downloads Directory:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
ttk.Entry(frame, textvariable=self.vars['downloads_dir'], width=40).grid(row=1, column=1, pady=5)
|
||||
ttk.Button(frame, text="Browse",
|
||||
command=lambda: self._browse_directory('downloads_dir')).grid(row=1, column=2, padx=5)
|
||||
|
||||
# Create directories checkbox
|
||||
self.create_dirs_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(frame, text="Create directories if they don't exist",
|
||||
variable=self.create_dirs_var).grid(row=2, column=0, columnspan=3, pady=10)
|
||||
|
||||
def _build_model_tab(self, parent: ttk.Frame):
|
||||
"""Build model defaults tab."""
|
||||
frame = ttk.LabelFrame(parent, text="Default Model Settings", padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Context size
|
||||
ttk.Label(frame, text="Default Context Size (n_ctx):").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
ctx_spin = tk.Spinbox(frame, from_=512, to=32768, increment=512,
|
||||
textvariable=self.vars['default_n_ctx'], width=15)
|
||||
ctx_spin.grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# GPU layers
|
||||
ttk.Label(frame, text="Default GPU Layers:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
gpu_spin = tk.Spinbox(frame, from_=0, to=100, increment=1,
|
||||
textvariable=self.vars['default_n_gpu'], width=15)
|
||||
gpu_spin.grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Max tokens
|
||||
ttk.Label(frame, text="Default Max Tokens:").grid(row=2, column=0, sticky=tk.W, pady=5)
|
||||
tokens_spin = tk.Spinbox(frame, from_=16, to=8192, increment=16,
|
||||
textvariable=self.vars['default_max_tokens'], width=15)
|
||||
tokens_spin.grid(row=2, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Stream by default
|
||||
ttk.Checkbutton(frame, text="Stream output by default",
|
||||
variable=self.vars['stream_default']).grid(row=3, column=0, columnspan=2, pady=10)
|
||||
|
||||
# Temperature
|
||||
ttk.Label(frame, text="Temperature:").grid(row=4, column=0, sticky=tk.W, pady=5)
|
||||
temp_spin = tk.Spinbox(frame, from_=0.0, to=2.0, increment=0.1,
|
||||
textvariable=self.vars['temperature'], width=15, format="%.1f")
|
||||
temp_spin.grid(row=4, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Top P
|
||||
ttk.Label(frame, text="Top P:").grid(row=5, column=0, sticky=tk.W, pady=5)
|
||||
top_p_spin = tk.Spinbox(frame, from_=0.0, to=1.0, increment=0.1,
|
||||
textvariable=self.vars['top_p'], width=15, format="%.1f")
|
||||
top_p_spin.grid(row=5, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Repetition Penalty
|
||||
ttk.Label(frame, text="Repetition Penalty:").grid(row=6, column=0, sticky=tk.W, pady=5)
|
||||
rep_pen_spin = tk.Spinbox(frame, from_=0.5, to=2.0, increment=0.1,
|
||||
textvariable=self.vars['repetition_penalty'], width=15, format="%.1f")
|
||||
rep_pen_spin.grid(row=6, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# No Repeat N-gram Size
|
||||
ttk.Label(frame, text="No Repeat N-gram Size:").grid(row=7, column=0, sticky=tk.W, pady=5)
|
||||
ngram_spin = tk.Spinbox(frame, from_=0, to=10, increment=1,
|
||||
textvariable=self.vars['no_repeat_ngram_size'], width=15)
|
||||
ngram_spin.grid(row=7, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Min P
|
||||
ttk.Label(frame, text="Min P:").grid(row=8, column=0, sticky=tk.W, pady=5)
|
||||
min_p_spin = tk.Spinbox(frame, from_=0.0, to=1.0, increment=0.01,
|
||||
textvariable=self.vars['min_p'], width=15, format="%.2f")
|
||||
min_p_spin.grid(row=8, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Typical P
|
||||
ttk.Label(frame, text="Typical P:").grid(row=9, column=0, sticky=tk.W, pady=5)
|
||||
typical_p_spin = tk.Spinbox(frame, from_=0.0, to=1.0, increment=0.1,
|
||||
textvariable=self.vars['typical_p'], width=15, format="%.1f")
|
||||
typical_p_spin.grid(row=9, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
def _build_search_tab(self, parent: ttk.Frame):
|
||||
"""Build search settings tab."""
|
||||
frame = ttk.LabelFrame(parent, text="Search Preferences", padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Default search type
|
||||
ttk.Label(frame, text="Default Search Type:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
ttk.Combobox(frame, textvariable=self.vars['search_type'],
|
||||
values=["Models", "Datasets"], state="readonly", width=20).grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Default sort
|
||||
ttk.Label(frame, text="Default Sort By:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
ttk.Combobox(frame, textvariable=self.vars['default_sort'],
|
||||
values=["downloads", "likes", "lastModified"], state="readonly", width=20).grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Search limit
|
||||
ttk.Label(frame, text="Results Limit:").grid(row=2, column=0, sticky=tk.W, pady=5)
|
||||
limit_spin = tk.Spinbox(frame, from_=10, to=200, increment=10,
|
||||
textvariable=self.vars['search_limit'], width=20)
|
||||
limit_spin.grid(row=2, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Auto filter GGUF
|
||||
ttk.Checkbutton(frame, text="Automatically filter for GGUF files when downloading models",
|
||||
variable=self.vars['auto_filter_gguf']).grid(row=3, column=0, columnspan=2, pady=10)
|
||||
|
||||
def _build_ui_tab(self, parent: ttk.Frame):
|
||||
"""Build UI preferences tab."""
|
||||
frame = ttk.LabelFrame(parent, text="Interface Settings", padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Theme selection
|
||||
ttk.Label(frame, text="Theme:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
ttk.Combobox(frame, textvariable=self.vars['theme'],
|
||||
values=["default", "dark", "light"], state="readonly", width=20).grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||
|
||||
# Show tooltips
|
||||
ttk.Checkbutton(frame, text="Show tooltips",
|
||||
variable=self.vars['show_tooltips']).grid(row=1, column=0, columnspan=2, pady=10)
|
||||
|
||||
# Note about themes
|
||||
ttk.Label(frame, text="Note: Theme changes will take effect after restart",
|
||||
foreground="gray").grid(row=2, column=0, columnspan=2, pady=5)
|
||||
|
||||
def _build_library_tab(self, parent: ttk.Frame):
|
||||
"""Build library settings tab."""
|
||||
frame = ttk.LabelFrame(parent, text="Model Library Settings", padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# Library root folder
|
||||
ttk.Label(frame, text="Library Root Folder:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
root_frame = ttk.Frame(frame)
|
||||
root_frame.grid(row=0, column=1, columnspan=2, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Entry(root_frame, textvariable=self.vars['library_root'], width=40).pack(side=tk.LEFT)
|
||||
ttk.Button(root_frame, text="Browse",
|
||||
command=lambda: self._browse_directory('library_root')).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Scan depth
|
||||
ttk.Label(frame, text="Maximum Scan Depth:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
depth_frame = ttk.Frame(frame)
|
||||
depth_frame.grid(row=1, column=1, columnspan=2, sticky=tk.W, pady=5)
|
||||
|
||||
depth_scale = ttk.Scale(depth_frame, from_=1, to=10, variable=self.vars['library_depth'],
|
||||
orient=tk.HORIZONTAL, length=200)
|
||||
depth_scale.pack(side=tk.LEFT)
|
||||
|
||||
depth_label = ttk.Label(depth_frame, textvariable=self.vars['library_depth'])
|
||||
depth_label.pack(side=tk.LEFT, padx=10)
|
||||
ttk.Label(depth_frame, text="levels").pack(side=tk.LEFT)
|
||||
|
||||
# Auto scan options
|
||||
ttk.Checkbutton(frame, text="Auto-scan library on startup",
|
||||
variable=self.vars['auto_scan']).grid(row=2, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
ttk.Checkbutton(frame, text="Watch for file system changes (experimental)",
|
||||
variable=self.vars['watch_changes']).grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=5)
|
||||
|
||||
# Info text
|
||||
info_text = ("The library scanner searches for model files in the specified folder.\n"
|
||||
"Scan depth controls how many subdirectory levels to search.\n"
|
||||
"Supported formats: .gguf, .bin, .safetensors, .pt, .pth, .onnx")
|
||||
ttk.Label(frame, text=info_text, foreground="gray").grid(row=4, column=0, columnspan=3, pady=10)
|
||||
|
||||
def _build_download_tab(self, parent: ttk.Frame):
|
||||
"""Build download settings tab."""
|
||||
# Download limits frame
|
||||
limits_frame = ttk.LabelFrame(parent, text="Download Limits", padding=10)
|
||||
limits_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
# Max concurrent downloads
|
||||
ttk.Label(limits_frame, text="Maximum Concurrent Downloads:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
concurrent_spin = tk.Spinbox(limits_frame, from_=1, to=10, increment=1,
|
||||
textvariable=self.vars['max_downloads'], width=15)
|
||||
concurrent_spin.grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(limits_frame, text="(1-10 downloads)").grid(row=0, column=2, sticky=tk.W, padx=5)
|
||||
|
||||
# Speed limits frame
|
||||
speed_frame = ttk.LabelFrame(parent, text="Speed Limits (KB/s)", padding=10)
|
||||
speed_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
# Max download speed
|
||||
ttk.Label(speed_frame, text="Maximum Download Speed:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
max_speed_spin = tk.Spinbox(speed_frame, from_=0, to=100000, increment=100,
|
||||
textvariable=self.vars['max_speed'], width=15)
|
||||
max_speed_spin.grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(speed_frame, text="(0 = unlimited)").grid(row=0, column=2, sticky=tk.W, padx=5)
|
||||
|
||||
# Min download speed
|
||||
ttk.Label(speed_frame, text="Minimum Download Speed:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
min_speed_spin = tk.Spinbox(speed_frame, from_=0, to=10000, increment=10,
|
||||
textvariable=self.vars['min_speed'], width=15)
|
||||
min_speed_spin.grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(speed_frame, text="(0 = no minimum)").grid(row=1, column=2, sticky=tk.W, padx=5)
|
||||
|
||||
# Connection settings frame
|
||||
conn_frame = ttk.LabelFrame(parent, text="Connection Settings", padding=10)
|
||||
conn_frame.pack(fill=tk.X, padx=10, pady=10)
|
||||
|
||||
# Retry attempts
|
||||
ttk.Label(conn_frame, text="Retry Attempts:").grid(row=0, column=0, sticky=tk.W, pady=5)
|
||||
retry_spin = tk.Spinbox(conn_frame, from_=0, to=10, increment=1,
|
||||
textvariable=self.vars['retry_attempts'], width=15)
|
||||
retry_spin.grid(row=0, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(conn_frame, text="(number of retries on failure)").grid(row=0, column=2, sticky=tk.W, padx=5)
|
||||
|
||||
# Timeout
|
||||
ttk.Label(conn_frame, text="Connection Timeout:").grid(row=1, column=0, sticky=tk.W, pady=5)
|
||||
timeout_spin = tk.Spinbox(conn_frame, from_=5, to=300, increment=5,
|
||||
textvariable=self.vars['timeout_seconds'], width=15)
|
||||
timeout_spin.grid(row=1, column=1, sticky=tk.W, pady=5)
|
||||
ttk.Label(conn_frame, text="(seconds)").grid(row=1, column=2, sticky=tk.W, padx=5)
|
||||
|
||||
# Info text
|
||||
info_text = ("• Speed limits help manage bandwidth usage\n"
|
||||
"• Concurrent downloads should be balanced with your internet connection\n"
|
||||
"• Higher timeout values help with slow connections")
|
||||
ttk.Label(parent, text=info_text, foreground="gray").pack(anchor=tk.W, padx=10, pady=10)
|
||||
|
||||
def _toggle_token_entry(self):
|
||||
"""Enable/disable token entry based on checkbox."""
|
||||
if self.vars['use_env_token'].get():
|
||||
self.token_entry.config(state="disabled")
|
||||
else:
|
||||
self.token_entry.config(state="normal")
|
||||
|
||||
def _toggle_token_visibility(self):
|
||||
"""Toggle token visibility."""
|
||||
if self.token_entry['show'] == "*":
|
||||
self.token_entry.config(show="")
|
||||
self.show_token_btn.config(text="Hide")
|
||||
else:
|
||||
self.token_entry.config(show="*")
|
||||
self.show_token_btn.config(text="View")
|
||||
|
||||
def _change_token(self):
|
||||
"""Open dialog to change API token."""
|
||||
import tkinter.simpledialog as simpledialog
|
||||
|
||||
new_token = simpledialog.askstring(
|
||||
"Change API Token",
|
||||
"Enter new HuggingFace API token:",
|
||||
parent=self.dialog,
|
||||
show='*'
|
||||
)
|
||||
|
||||
if new_token:
|
||||
self.vars['hf_token'].set(new_token)
|
||||
self.vars['use_env_token'].set(False)
|
||||
self._toggle_token_entry()
|
||||
self.api_status_label.config(text="Token updated. Click Test to verify.", foreground="blue")
|
||||
|
||||
def _test_api_key(self):
|
||||
"""Test the API key."""
|
||||
import requests
|
||||
|
||||
# Get the token to test
|
||||
if self.vars['use_env_token'].get():
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv("HUGGINGFACE.env")
|
||||
token = os.getenv("HF_API_KEY")
|
||||
else:
|
||||
token = self.vars['hf_token'].get()
|
||||
|
||||
if not token:
|
||||
self.api_status_label.config(text="No API token configured", foreground="red")
|
||||
return
|
||||
|
||||
try:
|
||||
# Test API by fetching user info
|
||||
# Ensure token is properly stripped of whitespace and newlines
|
||||
clean_token = token.strip().replace('\n', '').replace('\r', '')
|
||||
headers = {"Authorization": f"Bearer {clean_token}"}
|
||||
response = requests.get("https://huggingface.co/api/whoami", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
username = user_data.get('name', 'Unknown')
|
||||
self.api_status_label.config(
|
||||
text=f"✓ API key valid. Logged in as: {username}",
|
||||
foreground="green"
|
||||
)
|
||||
|
||||
# Update organizations if found
|
||||
orgs = user_data.get('orgs', [])
|
||||
if orgs:
|
||||
org_names = [org.get('name', '') for org in orgs]
|
||||
self.org_listbox.delete(0, tk.END)
|
||||
for org in org_names:
|
||||
self.org_listbox.insert(tk.END, org)
|
||||
self.org_combo['values'] = org_names
|
||||
|
||||
elif response.status_code == 401:
|
||||
self.api_status_label.config(text="✗ Invalid API token", foreground="red")
|
||||
else:
|
||||
self.api_status_label.config(
|
||||
text=f"✗ API test failed: {response.status_code}",
|
||||
foreground="red"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.api_status_label.config(text=f"✗ Connection error: {str(e)[:50]}", foreground="red")
|
||||
|
||||
def _toggle_organization(self):
|
||||
"""Enable/disable organization controls."""
|
||||
if self.vars['use_organization'].get():
|
||||
self.org_combo.config(state="readonly")
|
||||
if not self.org_combo['values']:
|
||||
self._fetch_organizations()
|
||||
else:
|
||||
self.org_combo.config(state="disabled")
|
||||
|
||||
def _fetch_organizations(self):
|
||||
"""Fetch organizations for the current API key."""
|
||||
import requests
|
||||
|
||||
# Get the token
|
||||
if self.vars['use_env_token'].get():
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv("HUGGINGFACE.env")
|
||||
token = os.getenv("HF_API_KEY")
|
||||
else:
|
||||
token = self.vars['hf_token'].get()
|
||||
|
||||
if not token:
|
||||
messagebox.showwarning("No Token", "Please configure an API token first")
|
||||
return
|
||||
|
||||
try:
|
||||
# Ensure token is properly stripped of whitespace and newlines
|
||||
clean_token = token.strip().replace('\n', '').replace('\r', '')
|
||||
headers = {"Authorization": f"Bearer {clean_token}"}
|
||||
response = requests.get("https://huggingface.co/api/whoami", headers=headers)
|
||||
|
||||
if response.status_code == 200:
|
||||
user_data = response.json()
|
||||
orgs = user_data.get('orgs', [])
|
||||
|
||||
if orgs:
|
||||
org_names = [org.get('name', '') for org in orgs]
|
||||
self.org_listbox.delete(0, tk.END)
|
||||
for org in org_names:
|
||||
self.org_listbox.insert(tk.END, org)
|
||||
self.org_combo['values'] = org_names
|
||||
|
||||
if org_names:
|
||||
self.org_combo.set(org_names[0])
|
||||
self.api_status_label.config(
|
||||
text=f"Found {len(org_names)} organization(s)",
|
||||
foreground="green"
|
||||
)
|
||||
else:
|
||||
self.api_status_label.config(
|
||||
text="No organizations found for this account",
|
||||
foreground="blue"
|
||||
)
|
||||
else:
|
||||
self.api_status_label.config(
|
||||
text=f"Failed to fetch organizations: {response.status_code}",
|
||||
foreground="red"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to fetch organizations: {str(e)}")
|
||||
|
||||
def _on_org_select(self, event):
|
||||
"""Handle organization selection from listbox."""
|
||||
selection = self.org_listbox.curselection()
|
||||
if selection:
|
||||
org_name = self.org_listbox.get(selection[0])
|
||||
self.vars['organization'].set(org_name)
|
||||
|
||||
def _browse_directory(self, var_name: str):
|
||||
"""Browse for directory."""
|
||||
directory = filedialog.askdirectory(
|
||||
parent=self.dialog,
|
||||
initialdir=self.vars[var_name].get() or "."
|
||||
)
|
||||
if directory:
|
||||
self.vars[var_name].set(directory)
|
||||
|
||||
def _load_current_settings(self):
|
||||
"""Load current settings into UI variables."""
|
||||
self.vars['hf_token'].set(self.settings.get('api.huggingface_token', ''))
|
||||
self.vars['use_env_token'].set(self.settings.get('api.use_env_token', True))
|
||||
self.vars['use_organization'].set(self.settings.get('api.use_organization', False))
|
||||
self.vars['organization'].set(self.settings.get('api.organization', ''))
|
||||
|
||||
self.vars['models_dir'].set(self.settings.get('paths.models_directory', './models'))
|
||||
self.vars['downloads_dir'].set(self.settings.get('paths.downloads_directory', './downloads'))
|
||||
|
||||
self.vars['default_n_ctx'].set(self.settings.get('model_settings.default_n_ctx', 4096))
|
||||
self.vars['default_n_gpu'].set(self.settings.get('model_settings.default_n_gpu_layers', 0))
|
||||
self.vars['default_max_tokens'].set(self.settings.get('model_settings.default_max_tokens', 256))
|
||||
self.vars['stream_default'].set(self.settings.get('model_settings.stream_by_default', True))
|
||||
self.vars['temperature'].set(self.settings.get('model_settings.temperature', 0.7))
|
||||
self.vars['top_p'].set(self.settings.get('model_settings.top_p', 0.9))
|
||||
self.vars['repetition_penalty'].set(self.settings.get('model_settings.repetition_penalty', 1.1))
|
||||
self.vars['no_repeat_ngram_size'].set(self.settings.get('model_settings.no_repeat_ngram_size', 0))
|
||||
self.vars['min_p'].set(self.settings.get('model_settings.min_p', 0.0))
|
||||
self.vars['typical_p'].set(self.settings.get('model_settings.typical_p', 1.0))
|
||||
|
||||
self.vars['search_type'].set(self.settings.get('search_preferences.default_search_type', 'Models'))
|
||||
self.vars['default_sort'].set(self.settings.get('search_preferences.default_sort', 'downloads'))
|
||||
self.vars['search_limit'].set(self.settings.get('search_preferences.search_limit', 50))
|
||||
self.vars['auto_filter_gguf'].set(self.settings.get('search_preferences.auto_filter_gguf', True))
|
||||
|
||||
self.vars['show_tooltips'].set(self.settings.get('ui_preferences.show_tooltips', True))
|
||||
self.vars['theme'].set(self.settings.get('ui_preferences.theme', 'default'))
|
||||
|
||||
# Library settings
|
||||
self.vars['library_root'].set(self.settings.get('library.root_folder', ''))
|
||||
self.vars['library_depth'].set(self.settings.get('library.max_depth', 3))
|
||||
self.vars['auto_scan'].set(self.settings.get('library.auto_scan_on_startup', False))
|
||||
self.vars['watch_changes'].set(self.settings.get('library.watch_for_changes', False))
|
||||
|
||||
# Download settings
|
||||
self.vars['max_downloads'].set(self.settings.get('download_settings.max_concurrent_downloads', 3))
|
||||
self.vars['max_speed'].set(self.settings.get('download_settings.max_download_speed', 0))
|
||||
self.vars['min_speed'].set(self.settings.get('download_settings.min_download_speed', 0))
|
||||
self.vars['retry_attempts'].set(self.settings.get('download_settings.retry_attempts', 3))
|
||||
self.vars['timeout_seconds'].set(self.settings.get('download_settings.timeout_seconds', 30))
|
||||
|
||||
# Update token entry state
|
||||
self._toggle_token_entry()
|
||||
|
||||
def _save_settings(self):
|
||||
"""Save settings from UI to settings manager."""
|
||||
# API settings (strip whitespace from strings)
|
||||
self.settings.set('api.huggingface_token', self.vars['hf_token'].get().strip())
|
||||
self.settings.set('api.use_env_token', self.vars['use_env_token'].get())
|
||||
self.settings.set('api.use_organization', self.vars['use_organization'].get())
|
||||
self.settings.set('api.organization', self.vars['organization'].get().strip())
|
||||
|
||||
# Path settings
|
||||
models_dir = self.vars['models_dir'].get()
|
||||
downloads_dir = self.vars['downloads_dir'].get()
|
||||
|
||||
# Create directories if requested
|
||||
if self.create_dirs_var.get():
|
||||
for directory in [models_dir, downloads_dir]:
|
||||
if directory and not os.path.exists(directory):
|
||||
try:
|
||||
os.makedirs(directory, exist_ok=True)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"Failed to create directory {directory}: {e}")
|
||||
|
||||
self.settings.set('paths.models_directory', models_dir)
|
||||
self.settings.set('paths.downloads_directory', downloads_dir)
|
||||
|
||||
# Model settings
|
||||
self.settings.set('model_settings.default_n_ctx', self.vars['default_n_ctx'].get())
|
||||
self.settings.set('model_settings.default_n_gpu_layers', self.vars['default_n_gpu'].get())
|
||||
self.settings.set('model_settings.default_max_tokens', self.vars['default_max_tokens'].get())
|
||||
self.settings.set('model_settings.stream_by_default', self.vars['stream_default'].get())
|
||||
self.settings.set('model_settings.temperature', self.vars['temperature'].get())
|
||||
self.settings.set('model_settings.top_p', self.vars['top_p'].get())
|
||||
self.settings.set('model_settings.repetition_penalty', self.vars['repetition_penalty'].get())
|
||||
self.settings.set('model_settings.no_repeat_ngram_size', self.vars['no_repeat_ngram_size'].get())
|
||||
self.settings.set('model_settings.min_p', self.vars['min_p'].get())
|
||||
self.settings.set('model_settings.typical_p', self.vars['typical_p'].get())
|
||||
|
||||
# Search settings
|
||||
self.settings.set('search_preferences.default_search_type', self.vars['search_type'].get())
|
||||
self.settings.set('search_preferences.default_sort', self.vars['default_sort'].get())
|
||||
self.settings.set('search_preferences.search_limit', self.vars['search_limit'].get())
|
||||
self.settings.set('search_preferences.auto_filter_gguf', self.vars['auto_filter_gguf'].get())
|
||||
|
||||
# UI settings
|
||||
self.settings.set('ui_preferences.show_tooltips', self.vars['show_tooltips'].get())
|
||||
self.settings.set('ui_preferences.theme', self.vars['theme'].get())
|
||||
|
||||
# Library settings
|
||||
self.settings.set('library.root_folder', self.vars['library_root'].get().strip())
|
||||
self.settings.set('library.max_depth', self.vars['library_depth'].get())
|
||||
self.settings.set('library.auto_scan_on_startup', self.vars['auto_scan'].get())
|
||||
self.settings.set('library.watch_for_changes', self.vars['watch_changes'].get())
|
||||
|
||||
# Download settings
|
||||
self.settings.set('download_settings.max_concurrent_downloads', self.vars['max_downloads'].get())
|
||||
self.settings.set('download_settings.max_download_speed', self.vars['max_speed'].get())
|
||||
self.settings.set('download_settings.min_download_speed', self.vars['min_speed'].get())
|
||||
self.settings.set('download_settings.retry_attempts', self.vars['retry_attempts'].get())
|
||||
self.settings.set('download_settings.timeout_seconds', self.vars['timeout_seconds'].get())
|
||||
|
||||
# Save to file
|
||||
if self.settings.save_settings():
|
||||
messagebox.showinfo("Settings", "Settings saved successfully!")
|
||||
self.dialog.destroy()
|
||||
else:
|
||||
messagebox.showerror("Error", "Failed to save settings")
|
||||
|
||||
def _reset_defaults(self):
|
||||
"""Reset settings to defaults."""
|
||||
if messagebox.askyesno("Reset Settings", "Are you sure you want to reset all settings to defaults?"):
|
||||
self.settings.reset_to_defaults()
|
||||
self._load_current_settings()
|
||||
messagebox.showinfo("Settings", "Settings reset to defaults")
|
||||
|
||||
def _center_window(self):
|
||||
"""Center the dialog on the parent window."""
|
||||
self.dialog.update_idletasks()
|
||||
|
||||
# Get parent position
|
||||
parent_x = self.parent.winfo_x()
|
||||
parent_y = self.parent.winfo_y()
|
||||
parent_width = self.parent.winfo_width()
|
||||
parent_height = self.parent.winfo_height()
|
||||
|
||||
# Get dialog size
|
||||
dialog_width = self.dialog.winfo_width()
|
||||
dialog_height = self.dialog.winfo_height()
|
||||
|
||||
# Calculate position
|
||||
x = parent_x + (parent_width - dialog_width) // 2
|
||||
y = parent_y + (parent_height - dialog_height) // 2
|
||||
|
||||
self.dialog.geometry(f"+{x}+{y}")
|
||||
|
||||
|
||||
def open_settings_dialog(parent: tk.Tk, settings_manager: SettingsManager):
|
||||
"""Convenience function to open the settings dialog."""
|
||||
dialog = SettingsDialog(parent, settings_manager)
|
||||
return dialog
|
||||
Reference in New Issue
Block a user