Updated UI, .env settings are now persisted correctly

This commit is contained in:
2026-04-08 14:23:26 +02:00
parent 6b35ba3cf2
commit 2104bfc74a
+114 -3
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
JR SQL AI GUI (local Ollama, resilient version v5)
JR SQL AI GUI (local Ollama, resilient version v9)
Improvements over v4:
- auto-prefill BASE_MODEL / EXPERT_MODEL / EXTRA_MODELS from .env, .env.example and Modelfile
@@ -173,6 +173,16 @@ def detect_project_root(start: Path) -> Path:
return start
def resolve_path_from_value(root: Path, raw_value: str, default: Path) -> Path:
value = (raw_value or '').strip()
if not value:
return default.resolve()
p = Path(value).expanduser()
if not p.is_absolute():
p = (root / p)
return p.resolve()
def parse_modelfile_for_base(modelfile_path: Path) -> Tuple[str, str]:
"""
Returns: (base_model, source_hint)
@@ -215,7 +225,7 @@ def resolve_defaults(root: Path, explicit_modelfile: Optional[Path] = None, curr
env_example_path = root / ".env.example"
env_data = read_env_file(env_path)
env_example_data = read_env_file(env_example_path)
modelfile_path = explicit_modelfile.resolve() if explicit_modelfile else (root / "Modelfile").resolve()
default_modelfile_path = explicit_modelfile.resolve() if explicit_modelfile else (root / "Modelfile").resolve()
notes: List[str] = []
@@ -238,6 +248,29 @@ def resolve_defaults(root: Path, explicit_modelfile: Optional[Path] = None, curr
expert_model = pick("EXPERT_MODEL", "jr-sql-expert")
extra_models = pick("EXTRA_MODELS", "")
modelfile_raw = ""
modelfile_source = ""
if explicit_modelfile and str(explicit_modelfile).strip():
modelfile_path = explicit_modelfile.resolve()
modelfile_source = "GUI"
elif env_data.get("MODELFILE_PATH", "").strip():
modelfile_raw = env_data.get("MODELFILE_PATH", "").strip()
modelfile_path = resolve_path_from_value(root, modelfile_raw, default_modelfile_path)
modelfile_source = ".env"
elif env_example_data.get("MODELFILE_PATH", "").strip():
modelfile_raw = env_example_data.get("MODELFILE_PATH", "").strip()
modelfile_path = resolve_path_from_value(root, modelfile_raw, default_modelfile_path)
modelfile_source = ".env.example"
elif os.environ.get("MODELFILE_PATH", "").strip():
modelfile_raw = os.environ.get("MODELFILE_PATH", "").strip()
modelfile_path = resolve_path_from_value(root, modelfile_raw, default_modelfile_path)
modelfile_source = "Umgebung"
else:
modelfile_path = default_modelfile_path
if modelfile_source:
notes.append(f"MODELFILE_PATH aus {modelfile_source}")
if not base_model:
modelfile_base, modelfile_note = parse_modelfile_for_base(modelfile_path)
if modelfile_base:
@@ -248,6 +281,7 @@ def resolve_defaults(root: Path, explicit_modelfile: Optional[Path] = None, curr
inference_model = (
current_inference_model.strip()
or pick("DEFAULT_INFERENCE_MODEL", "")
or os.environ.get("OLLAMA_MODEL", "").strip()
or (f"{expert_model}:latest" if expert_model else "jr-sql-expert:latest")
)
@@ -413,6 +447,8 @@ class MainWindow(QMainWindow):
self._cmd_worker: Optional[CommandWorker] = None
self._temp_modelfile: Optional[Path] = None
self._raw_markdown = ""
self._config_dirty = False
self._suspend_dirty_tracking = False
self.project_root = detect_project_root(Path(__file__).resolve().parent)
self.config = AppConfig.load(self.project_root)
@@ -478,6 +514,11 @@ class MainWindow(QMainWindow):
self.lbl_defaults_hint = QLabel("")
self.lbl_defaults_hint.setWordWrap(True)
config_layout.addWidget(self.lbl_defaults_hint, row, 0, 1, 5)
row += 1
self.lbl_config_state = QLabel("")
self.lbl_config_state.setWordWrap(True)
config_layout.addWidget(self.lbl_config_state, row, 0, 1, 5)
top = QHBoxLayout()
layout.addLayout(top)
@@ -579,9 +620,54 @@ class MainWindow(QMainWindow):
self.setStatusBar(self.status)
self.status.showMessage("Bereit.")
self._connect_config_change_tracking()
self.apply_default_hints()
self._set_config_dirty(False)
QTimer.singleShot(300, self.refresh_models)
def _connect_config_change_tracking(self) -> None:
fields = [
self.project_root_edit,
self.base_url,
self.ollama_bin,
self.base_model_edit,
self.expert_model_edit,
self.extra_models_edit,
self.modelfile_edit,
]
for field in fields:
field.textEdited.connect(self._on_config_field_edited)
self.model.lineEdit().textEdited.connect(self._on_config_field_edited)
self.model.currentTextChanged.connect(self._on_inference_model_changed)
def _set_config_dirty(self, dirty: bool, reason: str = "") -> None:
if self._suspend_dirty_tracking:
return
self._config_dirty = dirty
if dirty:
self.lbl_config_state.setText(
"Konfiguration geändert und noch nicht gespeichert. Klicke auf '.env speichern', um die Änderungen dauerhaft zu übernehmen."
)
self.lbl_config_state.setStyleSheet("color: #f6c177; font-weight: 600;")
self.btn_save_env.setText(".env speichern *")
self.setWindowTitle("JR SQL AI GUI (local Ollama) *")
if reason:
self.status.showMessage(reason, 5000)
else:
self.lbl_config_state.setText("Konfiguration ist gespeichert bzw. entspricht dem geladenen Stand.")
self.lbl_config_state.setStyleSheet("color: #9ccfd8;")
self.btn_save_env.setText(".env speichern")
self.setWindowTitle("JR SQL AI GUI (local Ollama)")
def _on_config_field_edited(self, _text: str) -> None:
self._set_config_dirty(True, "Ungespeicherte Konfigurationsänderungen vorhanden.")
def _on_inference_model_changed(self, text: str) -> None:
if self._suspend_dirty_tracking:
return
if text.strip():
self._set_config_dirty(True, "Antwortmodell geändert. Speichere die .env, wenn dies der neue Standard sein soll.")
def _apply_dark_menu_style(self) -> None:
menu_bar = self.menuBar()
menu_bar.setNativeMenuBar(False)
@@ -666,6 +752,8 @@ class MainWindow(QMainWindow):
cfg = self.current_config()
defaults = resolve_defaults(cfg.project_root, cfg.modelfile_path, cfg.default_inference_model)
self._suspend_dirty_tracking = True
try:
if not self.base_url.text().strip() and defaults.ollama_url:
self.base_url.setText(defaults.ollama_url)
if not self.ollama_bin.text().strip() and defaults.ollama_bin:
@@ -680,6 +768,8 @@ class MainWindow(QMainWindow):
self.modelfile_edit.setText(str(defaults.modelfile_path))
if not self.model.currentText().strip() and defaults.inference_model:
self.model.setCurrentText(defaults.inference_model)
finally:
self._suspend_dirty_tracking = False
self.apply_default_hints()
@@ -721,10 +811,13 @@ class MainWindow(QMainWindow):
if path:
self.modelfile_edit.setText(path)
self.autofill_missing_fields_from_disk()
self._set_config_dirty(True, "Modelfile-Pfad geändert. Speichere die .env, um den neuen Pfad dauerhaft zu übernehmen.")
def reload_config_from_disk(self) -> None:
root = Path(self.project_root_edit.text().strip() or self.project_root).resolve()
self.config = AppConfig.load(root)
self._suspend_dirty_tracking = True
try:
self.project_root_edit.setText(str(self.config.project_root))
self.base_url.setText(self.config.ollama_url)
self.ollama_bin.setText(self.config.ollama_bin)
@@ -732,9 +825,11 @@ class MainWindow(QMainWindow):
self.expert_model_edit.setText(self.config.expert_model)
self.extra_models_edit.setText(self.config.extra_models)
self.modelfile_edit.setText(str(self.config.modelfile_path))
if not self.model.currentText().strip():
self.model.setCurrentText(self.config.default_inference_model)
finally:
self._suspend_dirty_tracking = False
self.apply_default_hints()
self._set_config_dirty(False)
self.status.showMessage("Konfiguration neu geladen.", 2500)
self.refresh_models()
@@ -749,9 +844,12 @@ class MainWindow(QMainWindow):
"BASE_MODEL": cfg.base_model,
"EXPERT_MODEL": cfg.expert_model,
"EXTRA_MODELS": cfg.extra_models,
"MODELFILE_PATH": str(cfg.modelfile_path),
"DEFAULT_INFERENCE_MODEL": cfg.default_inference_model,
},
)
self.apply_default_hints()
self._set_config_dirty(False)
self.status.showMessage(f".env gespeichert: {cfg.env_path}", 3500)
# ----------- UI helpers -----------
@@ -1077,6 +1175,19 @@ class MainWindow(QMainWindow):
self.msg_info("Bitte warten", "Es läuft gerade eine Wartungsaktion. Schließe das Fenster danach erneut.")
event.ignore()
return
if self._config_dirty:
answer = QMessageBox.question(
self,
"Ungespeicherte Konfiguration",
"Es gibt ungespeicherte Konfigurationsänderungen. Möchtest du sie vor dem Beenden in die .env schreiben?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
QMessageBox.Yes,
)
if answer == QMessageBox.Cancel:
event.ignore()
return
if answer == QMessageBox.Yes:
self.save_env()
event.accept()