Updated UI, .env settings are now persisted correctly
This commit is contained in:
+114
-3
@@ -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()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user