diff --git a/interface/app.py b/interface/app.py index 356bde5..c901652 100644 --- a/interface/app.py +++ b/interface/app.py @@ -119,7 +119,7 @@ psutil = None -APP_NAME = "DrunkenBot LLM-IDE" +APP_NAME = "DrunkenBot-IDE" # Bump on every release that should require a version-ceiling check against # licenses -- this is what license_client.check_license_at_launch compares # against a license's version_ceiling/grace_period_until. @@ -195,35 +195,31 @@ def _ensure_valid_license(splash: "StartupValidationSplash") -> bool: "[OK] License valid" + (" (offline grace period)" if result.used_offline_grace else "") ) + QApplication.instance().setProperty("license_valid", True) return True initial_message = result.reason else: initial_message = "No license activated on this machine yet." - # A QSplashScreen-style window is designed to stay on top of other - # windows during startup -- which means it can end up covering a newly - # created dialog instead of the other way around. Hide it while the - # dialog is up rather than fight window-stacking order; it isn't doing - # anything useful to look at during activation anyway. splash.hide() try: - while True: - dialog = LicenseActivationDialog(APP_VERSION, LICENSE_SERVER_URL, initial_message) - dialog.setWindowIcon(MainWindow._static_app_icon()) - dialog.show() - dialog.raise_() - dialog.activateWindow() - if dialog.exec() != QDialog.Accepted: - LOGGER.info("License activation cancelled by user; exiting.") - QApplication.instance().setProperty("startup_aborted", True) - return False + dialog = LicenseActivationDialog(APP_VERSION, LICENSE_SERVER_URL, initial_message) + dialog.setWindowIcon(MainWindow._static_app_icon()) + if dialog.exec() == QDialog.Accepted and dialog.result_info is not None: + QApplication.instance().setProperty("license_valid", True) splash.append_log("[OK] License activated") return True + if dialog.trial_requested: + QApplication.instance().setProperty("license_valid", False) + splash.append_log("[TRIAL] User selected trial version") + return True + LOGGER.info("License activation cancelled by user; exiting.") + QApplication.instance().setProperty("startup_aborted", True) + return False finally: splash.show() splash.raise_() - def main(app: Optional[QApplication] = None, splash: Optional[StartupSplash] = None) -> None: """Launch the PySide6 desktop application.""" diff --git a/interface/license_activation_dialog.py b/interface/license_activation_dialog.py index 0956945..d89147c 100644 --- a/interface/license_activation_dialog.py +++ b/interface/license_activation_dialog.py @@ -105,8 +105,9 @@ def __init__(self, app_version: str, server_url: str, initial_message: str = "") self._app_version = app_version self._server_url = server_url self.result_info: LicenseCheckResult | None = None + self.trial_requested = False - self.setWindowTitle("Activate DrunkenBot LLM-IDE") + self.setWindowTitle("Activate DrunkenBot-IDE") self.setModal(True) self.setMinimumWidth(480) @@ -132,7 +133,10 @@ def __init__(self, app_version: str, server_url: str, initial_message: str = "") self._activate_button.clicked.connect(self._on_activate_clicked) self._exit_button = QPushButton("Exit") self._exit_button.clicked.connect(self.reject) + self._trial_button = QPushButton("Open Trial Version") + self._trial_button.clicked.connect(self._open_trial) button_row.addWidget(self._activate_button) + button_row.addWidget(self._trial_button) button_row.addWidget(self._exit_button) layout.addLayout(button_row) @@ -162,6 +166,11 @@ def _on_activate_clicked(self) -> None: self._show_status(result.reason) + def _open_trial(self) -> None: + """Close the dialog and request restricted trial mode.""" + self.trial_requested = True + self.accept() + def _show_status(self, message: str) -> None: """Display a status/error message in the dialog. diff --git a/interface/main_window_part1.py b/interface/main_window_part1.py index c149431..45fc0d3 100644 --- a/interface/main_window_part1.py +++ b/interface/main_window_part1.py @@ -15,7 +15,11 @@ def __init__(self) -> None: LOGGER.info("Creating %s main window", APP_NAME) if QApplication.instance(): QApplication.instance().setFont(QFont("Arial", 10)) - self.setWindowTitle(APP_NAME) + licensed = bool(QApplication.instance().property("license_valid")) + self.setWindowTitle( + f"{APP_NAME} {APP_VERSION} " + f"({'licensed' if licensed else 'Trial Version'})" + ) self.setWindowIcon(self._app_icon()) self._windows_icon_handles: list[int] = [] self.resize(1240, 820) @@ -372,7 +376,11 @@ def show_chat_only_mode(self) -> None: if hasattr(self, "side_rail"): self.side_rail.hide() self._switch_page(8) - self.setWindowTitle("DrunkenBot - Chat") + licensed = bool(QApplication.instance().property("license_valid")) + self.setWindowTitle( + f"{APP_NAME} {APP_VERSION} " + f"({'licensed' if licensed else 'Trial Version'})" + ) self.resize(980, 760) def resizeEvent(self, event: Any) -> None: diff --git a/interface/main_window_part11.py b/interface/main_window_part11.py index 5f09168..c6f2c7e 100644 --- a/interface/main_window_part11.py +++ b/interface/main_window_part11.py @@ -8,6 +8,8 @@ class MainWindowPart11: def prepare_dataset(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + self.context_length.setValue(min(self.context_length.value(), 1000)) """Collect dataset options and start dataset preparation.""" config = self._dataset_config_from_ui() @@ -282,6 +284,11 @@ def _update_online_dataset_stage_controls(self) -> None: if not hasattr(self, "dataset_stage"): return + if not bool(QApplication.instance().property("license_valid")): + self.include_conversation_datasets.setChecked(False) + self.include_conversation_datasets.setEnabled(False) + if hasattr(self, "external_dataset_download_button"): + self.external_dataset_download_button.setEnabled(False) stage = self._dataset_stage_value() allowed = set(CONVERSATION_DATASET_PRESETS) include_online = self.include_conversation_datasets.isChecked() @@ -422,5 +429,3 @@ def _selected_default_data_paths(self) -> list[Path]: for path, item in self.default_data_actions.items() if item.checkState(0) == Qt.Checked ] - - diff --git a/interface/main_window_part16.py b/interface/main_window_part16.py index af37946..e38c512 100644 --- a/interface/main_window_part16.py +++ b/interface/main_window_part16.py @@ -312,6 +312,8 @@ def _run_training_preflight(self, model_config: ModelConfig, training_config: Tr return True def start_training(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + self.train_context_length.setValue(min(self.train_context_length.value(), 1000)) """Collect training options and start model training.""" launch_target = self._training_launch_target_value() @@ -388,4 +390,3 @@ def start_training(self) -> None: task_kind="training", ) - diff --git a/interface/main_window_part17.py b/interface/main_window_part17.py index 8361f28..64a3928 100644 --- a/interface/main_window_part17.py +++ b/interface/main_window_part17.py @@ -382,6 +382,9 @@ def _append_chat_markdown(self, role: str, content: str) -> None: self._add_chat_message("user" if role.lower() in {"you", "user"} else "assistant", content) def create_bundle(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + self.export_log.append("Export Bay is available only in the licensed version.") + return """Create a portable model export bundle.""" self.export_log.append("Creating model bundle...") @@ -397,6 +400,8 @@ def create_bundle(self) -> None: self.export_status.setText("Export: bundle created") def quantize_model(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + return """Create a quantized FP16 checkpoint when selected.""" mode = self.quant_mode.currentText() @@ -418,6 +423,8 @@ def quantize_model(self) -> None: self.export_status.setText("Export: FP16 checkpoint ready") def export_hf_package(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + return """Create an HF-style MicroGPT package.""" self.export_log.append("Creating HF-style MicroGPT package...") @@ -433,4 +440,3 @@ def export_hf_package(self) -> None: self.export_log.append("Note: this package is MicroGPT model_type, not a llama.cpp-supported Llama model.") self.export_status.setText("Export: HF package ready") - diff --git a/interface/main_window_part18.py b/interface/main_window_part18.py index 5662263..0a583e4 100644 --- a/interface/main_window_part18.py +++ b/interface/main_window_part18.py @@ -8,6 +8,8 @@ class MainWindowPart18: def export_llama_adapter(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + return """Create a directly loadable Llama-family package when compatible.""" self.export_log.append("Creating Llama-compatible adapter package...") @@ -23,6 +25,8 @@ def export_llama_adapter(self) -> None: self.export_status.setText("Export: Llama adapter ready") def convert_hf_to_gguf(self) -> None: + if not bool(QApplication.instance().property("license_valid")): + return """Convert an HF-compatible model folder to GGUF through llama.cpp.""" model_dir_text = self.export_model_dir.text().strip() @@ -92,4 +96,3 @@ def _apply_preset(self, preset: str) -> None: self.n_layer.setValue(8) - diff --git a/interface/startup.py b/interface/startup.py index a8613d8..b116a06 100644 --- a/interface/startup.py +++ b/interface/startup.py @@ -34,7 +34,7 @@ ) -APP_NAME = "DrunkenBot LLM-IDE" +APP_NAME = "DrunkenBot-IDE" WINDOWS_APP_ID = "DrunkenBot.LLMIDE" LOGGER = logging.getLogger(__name__) APP_HOME_DIR = Path.home() / ".drunkenbot_ide" @@ -353,7 +353,7 @@ def _build_ui(self) -> None: body = QLabel( "Startup checks are complete.\n" - "Choose how you want to begin with DrunkenBot LLM-IDE." + "Choose how you want to begin with DrunkenBot-IDE." ) body.setObjectName("Body") body.setAlignment(Qt.AlignLeft) diff --git a/interface/startup_splash.py b/interface/startup_splash.py index a8fa826..64c7b02 100644 --- a/interface/startup_splash.py +++ b/interface/startup_splash.py @@ -15,7 +15,7 @@ class StartupSplash(QDialog): def __init__(self) -> None: super().__init__() - self.setWindowTitle("DrunkenBot LLM-IDE") + self.setWindowTitle("DrunkenBot-IDE") self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setModal(True) self.setMinimumSize(560, 760) @@ -42,7 +42,7 @@ def __init__(self) -> None: else: logo.setPixmap(pixmap.scaled(118, 118, Qt.KeepAspectRatio, Qt.SmoothTransformation)) logo.setAlignment(Qt.AlignCenter) - title = QLabel("DrunkenBot LLM-IDE") + title = QLabel("DrunkenBot-IDE") title.setObjectName("Title") title.setFont(QFont("Arial", 22)) header.addWidget(logo) diff --git a/interface/tabs/dataset_plan_tab.py b/interface/tabs/dataset_plan_tab.py index 196f550..a9d5ef2 100644 --- a/interface/tabs/dataset_plan_tab.py +++ b/interface/tabs/dataset_plan_tab.py @@ -6,6 +6,7 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( + QApplication, QCheckBox, QComboBox, QFormLayout, @@ -265,6 +266,8 @@ def build_dataset_plan_tab(window) -> QWidget: window.external_dataset_download_button = QPushButton("Download latest dataset") window._tip(window.external_dataset_download_button, "Download, verify, and extract the latest dataset release into the install folder.") window.external_dataset_download_button.clicked.connect(window.download_latest_external_dataset) + trial_mode = not bool(QApplication.instance().property("license_valid")) + window.external_dataset_download_button.setEnabled(not trial_mode) external_form.addRow("Install folder", window._path_row(window.external_dataset_dir, directory=True)) external_form.addRow("Status", window.external_dataset_version) external_form.addRow("", window.external_dataset_download_button) @@ -287,6 +290,7 @@ def build_dataset_plan_tab(window) -> QWidget: window.dataset_stage.setMaximumWidth(240) window.include_conversation_datasets = QCheckBox("Online") window.include_conversation_datasets.setChecked(False) + window.include_conversation_datasets.setEnabled(not trial_mode) purpose_row = QWidget() purpose_layout = QHBoxLayout(purpose_row) purpose_layout.setContentsMargins(0, 0, 0, 0) @@ -320,6 +324,7 @@ def build_dataset_plan_tab(window) -> QWidget: conversation_form.addRow("Custom HF dataset", window.custom_huggingface_dataset) window.custom_huggingface_download = QPushButton("Download custom dataset") window.custom_huggingface_download.clicked.connect(window._download_custom_huggingface_dataset) + window.custom_huggingface_download.setEnabled(not trial_mode) window._tip(window.custom_huggingface_download, "Enable the custom dataset and download it during the next dataset preparation run.") conversation_form.addRow("", window.custom_huggingface_download) window.conversation_sample_limit = window._spin(0, 2_000_000, 20000) @@ -455,4 +460,3 @@ def populate_default_data_tree(window: Any, root: Path) -> None: tree.blockSignals(False) if not window.default_data_actions: tree.addTopLevelItem(QTreeWidgetItem(["No project/default data files were found.", "", ""])) - diff --git a/interface/tabs/dataset_tab.py b/interface/tabs/dataset_tab.py index 6287428..6a8bfdc 100644 --- a/interface/tabs/dataset_tab.py +++ b/interface/tabs/dataset_tab.py @@ -111,7 +111,7 @@ def build_dataset_tab(window) -> QWidget: window._tip(window.auto_vocab_label, "The actual vocabulary size selected after reading the corpus.") window.min_frequency = window._spin(1, 1000, 2) window._tip(window.min_frequency, "Minimum token frequency for tokenizer training. Higher values remove rare fragments and can reduce noise.") - window.context_length = window._spin(16, 4096, 128) + window.context_length = window._spin(16, 1000, 128) window._tip(window.context_length, "Number of tokens per training sequence. Longer context lets the model learn longer dependencies but uses more memory.") window.validation_split = window._double_spin(0.0, 0.5, 0.1, 0.01, 3) window._tip(window.validation_split, "Fraction of tokens held out for validation. Validation helps detect overfitting during training.") diff --git a/interface/tabs/export_tab.py b/interface/tabs/export_tab.py index 3a07eef..8abd403 100644 --- a/interface/tabs/export_tab.py +++ b/interface/tabs/export_tab.py @@ -4,6 +4,7 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import ( + QApplication, QComboBox, QFormLayout, QHBoxLayout, @@ -84,6 +85,10 @@ def build_export_tab(window) -> QWidget: window.gguf_convert_button = QPushButton("Convert HF to GGUF") window._tip(window.gguf_convert_button, "Run llama.cpp convert_hf_to_gguf.py for model_core/hf_model when the architecture is supported by llama.cpp.") window.gguf_convert_button.clicked.connect(window.convert_hf_to_gguf) + window.export_buttons = [bundle_button, quant_button, hf_button, llama_button, window.gguf_convert_button] + if not bool(QApplication.instance().property("license_valid")): + for button in window.export_buttons: + button.setEnabled(False) bundle_button.setMaximumWidth(220) quant_button.setMaximumWidth(220) hf_button.setMaximumWidth(220) @@ -116,4 +121,3 @@ def build_export_tab(window) -> QWidget: window.export_progress = window._thin_progress() outer.addWidget(window.export_progress) return page - diff --git a/interface/tabs/training_tab.py b/interface/tabs/training_tab.py index 92b3e77..fa0960a 100644 --- a/interface/tabs/training_tab.py +++ b/interface/tabs/training_tab.py @@ -113,7 +113,7 @@ def build_training_tab(window) -> QWidget: window._tip(window.attention_window, "Sliding attention window. 0 uses full context; higher values restrict attention to recent tokens.") window.n_layer = window._spin(1, 64, 4) window._tip(window.n_layer, "Transformer layer count. More layers improve capacity and reasoning patterns but slow training.") - window.train_context_length = window._spin(16, 4096, 128) + window.train_context_length = window._spin(16, 1000, 128) window._tip(window.train_context_length, "Training context length in tokens. Must fit your GPU/CPU memory.") window.dropout = window._double_spin(0.0, 0.9, 0.1, 0.01, 3) window._tip(window.dropout, "Dropout regularization. Higher values reduce overfitting but can slow learning.") diff --git a/packager.py b/packager.py index 5d314a8..99bf90a 100644 --- a/packager.py +++ b/packager.py @@ -19,7 +19,7 @@ ROOT = Path(__file__).resolve().parent OUTPUT_ROOT = ROOT / "packaging" / "artifacts" -APP_NAME = "DrunkenBot-LLM-IDE" +APP_NAME = "DrunkenBot-IDE" def _architecture() -> str: