Updated UI, .env settings are now persisted correctly
This commit is contained in:
+114
-3
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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:
|
Improvements over v4:
|
||||||
- auto-prefill BASE_MODEL / EXPERT_MODEL / EXTRA_MODELS from .env, .env.example and Modelfile
|
- 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
|
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]:
|
def parse_modelfile_for_base(modelfile_path: Path) -> Tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Returns: (base_model, source_hint)
|
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_example_path = root / ".env.example"
|
||||||
env_data = read_env_file(env_path)
|
env_data = read_env_file(env_path)
|
||||||
env_example_data = read_env_file(env_example_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] = []
|
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")
|
expert_model = pick("EXPERT_MODEL", "jr-sql-expert")
|
||||||
extra_models = pick("EXTRA_MODELS", "")
|
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:
|
if not base_model:
|
||||||
modelfile_base, modelfile_note = parse_modelfile_for_base(modelfile_path)
|
modelfile_base, modelfile_note = parse_modelfile_for_base(modelfile_path)
|
||||||
if modelfile_base:
|
if modelfile_base:
|
||||||
@@ -248,6 +281,7 @@ def resolve_defaults(root: Path, explicit_modelfile: Optional[Path] = None, curr
|
|||||||
|
|
||||||
inference_model = (
|
inference_model = (
|
||||||
current_inference_model.strip()
|
current_inference_model.strip()
|
||||||
|
or pick("DEFAULT_INFERENCE_MODEL", "")
|
||||||
or os.environ.get("OLLAMA_MODEL", "").strip()
|
or os.environ.get("OLLAMA_MODEL", "").strip()
|
||||||
or (f"{expert_model}:latest" if expert_model else "jr-sql-expert:latest")
|
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._cmd_worker: Optional[CommandWorker] = None
|
||||||
self._temp_modelfile: Optional[Path] = None
|
self._temp_modelfile: Optional[Path] = None
|
||||||
self._raw_markdown = ""
|
self._raw_markdown = ""
|
||||||
|
self._config_dirty = False
|
||||||
|
self._suspend_dirty_tracking = False
|
||||||
|
|
||||||
self.project_root = detect_project_root(Path(__file__).resolve().parent)
|
self.project_root = detect_project_root(Path(__file__).resolve().parent)
|
||||||
self.config = AppConfig.load(self.project_root)
|
self.config = AppConfig.load(self.project_root)
|
||||||
@@ -478,6 +514,11 @@ class MainWindow(QMainWindow):
|
|||||||
self.lbl_defaults_hint = QLabel("")
|
self.lbl_defaults_hint = QLabel("")
|
||||||
self.lbl_defaults_hint.setWordWrap(True)
|
self.lbl_defaults_hint.setWordWrap(True)
|
||||||
config_layout.addWidget(self.lbl_defaults_hint, row, 0, 1, 5)
|
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()
|
top = QHBoxLayout()
|
||||||
layout.addLayout(top)
|
layout.addLayout(top)
|
||||||
@@ -579,9 +620,54 @@ class MainWindow(QMainWindow):
|
|||||||
self.setStatusBar(self.status)
|
self.setStatusBar(self.status)
|
||||||
self.status.showMessage("Bereit.")
|
self.status.showMessage("Bereit.")
|
||||||
|
|
||||||
|
self._connect_config_change_tracking()
|
||||||
self.apply_default_hints()
|
self.apply_default_hints()
|
||||||
|
self._set_config_dirty(False)
|
||||||
QTimer.singleShot(300, self.refresh_models)
|
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:
|
def _apply_dark_menu_style(self) -> None:
|
||||||
menu_bar = self.menuBar()
|
menu_bar = self.menuBar()
|
||||||
menu_bar.setNativeMenuBar(False)
|
menu_bar.setNativeMenuBar(False)
|
||||||
@@ -666,6 +752,8 @@ class MainWindow(QMainWindow):
|
|||||||
cfg = self.current_config()
|
cfg = self.current_config()
|
||||||
defaults = resolve_defaults(cfg.project_root, cfg.modelfile_path, cfg.default_inference_model)
|
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:
|
if not self.base_url.text().strip() and defaults.ollama_url:
|
||||||
self.base_url.setText(defaults.ollama_url)
|
self.base_url.setText(defaults.ollama_url)
|
||||||
if not self.ollama_bin.text().strip() and defaults.ollama_bin:
|
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))
|
self.modelfile_edit.setText(str(defaults.modelfile_path))
|
||||||
if not self.model.currentText().strip() and defaults.inference_model:
|
if not self.model.currentText().strip() and defaults.inference_model:
|
||||||
self.model.setCurrentText(defaults.inference_model)
|
self.model.setCurrentText(defaults.inference_model)
|
||||||
|
finally:
|
||||||
|
self._suspend_dirty_tracking = False
|
||||||
|
|
||||||
self.apply_default_hints()
|
self.apply_default_hints()
|
||||||
|
|
||||||
@@ -721,10 +811,13 @@ class MainWindow(QMainWindow):
|
|||||||
if path:
|
if path:
|
||||||
self.modelfile_edit.setText(path)
|
self.modelfile_edit.setText(path)
|
||||||
self.autofill_missing_fields_from_disk()
|
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:
|
def reload_config_from_disk(self) -> None:
|
||||||
root = Path(self.project_root_edit.text().strip() or self.project_root).resolve()
|
root = Path(self.project_root_edit.text().strip() or self.project_root).resolve()
|
||||||
self.config = AppConfig.load(root)
|
self.config = AppConfig.load(root)
|
||||||
|
self._suspend_dirty_tracking = True
|
||||||
|
try:
|
||||||
self.project_root_edit.setText(str(self.config.project_root))
|
self.project_root_edit.setText(str(self.config.project_root))
|
||||||
self.base_url.setText(self.config.ollama_url)
|
self.base_url.setText(self.config.ollama_url)
|
||||||
self.ollama_bin.setText(self.config.ollama_bin)
|
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.expert_model_edit.setText(self.config.expert_model)
|
||||||
self.extra_models_edit.setText(self.config.extra_models)
|
self.extra_models_edit.setText(self.config.extra_models)
|
||||||
self.modelfile_edit.setText(str(self.config.modelfile_path))
|
self.modelfile_edit.setText(str(self.config.modelfile_path))
|
||||||
if not self.model.currentText().strip():
|
|
||||||
self.model.setCurrentText(self.config.default_inference_model)
|
self.model.setCurrentText(self.config.default_inference_model)
|
||||||
|
finally:
|
||||||
|
self._suspend_dirty_tracking = False
|
||||||
self.apply_default_hints()
|
self.apply_default_hints()
|
||||||
|
self._set_config_dirty(False)
|
||||||
self.status.showMessage("Konfiguration neu geladen.", 2500)
|
self.status.showMessage("Konfiguration neu geladen.", 2500)
|
||||||
self.refresh_models()
|
self.refresh_models()
|
||||||
|
|
||||||
@@ -749,9 +844,12 @@ class MainWindow(QMainWindow):
|
|||||||
"BASE_MODEL": cfg.base_model,
|
"BASE_MODEL": cfg.base_model,
|
||||||
"EXPERT_MODEL": cfg.expert_model,
|
"EXPERT_MODEL": cfg.expert_model,
|
||||||
"EXTRA_MODELS": cfg.extra_models,
|
"EXTRA_MODELS": cfg.extra_models,
|
||||||
|
"MODELFILE_PATH": str(cfg.modelfile_path),
|
||||||
|
"DEFAULT_INFERENCE_MODEL": cfg.default_inference_model,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.apply_default_hints()
|
self.apply_default_hints()
|
||||||
|
self._set_config_dirty(False)
|
||||||
self.status.showMessage(f".env gespeichert: {cfg.env_path}", 3500)
|
self.status.showMessage(f".env gespeichert: {cfg.env_path}", 3500)
|
||||||
|
|
||||||
# ----------- UI helpers -----------
|
# ----------- 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.")
|
self.msg_info("Bitte warten", "Es läuft gerade eine Wartungsaktion. Schließe das Fenster danach erneut.")
|
||||||
event.ignore()
|
event.ignore()
|
||||||
return
|
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()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user