fix+ux(settings): clearer provider/model flow, inline provider editing
- Merge Anbieter and Standard-LLM-Parameter into one card with explicit
step labels (Schritt 1: Anbieter, Schritt 2: Modell setzen)
- Show current default provider name in step 2 so it's always visible
- Add inline edit form per provider row (pencil button toggles form)
- Add /providers/{id}/update and /providers/{id}/clear-key routes
- Fix sqlite3.Row.get() AttributeError in _prompt_llm_block and
_process_analysis_job (use .keys() instead)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
112
app.py
112
app.py
@@ -545,7 +545,7 @@ def _process_analysis_job(job_id: int):
|
||||
repeat_penalty = float(get_setting("llm_repeat_penalty", "1.15"))
|
||||
repeat_last_n = int(get_setting("llm_repeat_last_n", "128"))
|
||||
else:
|
||||
provider = _get_provider(prm.get("provider_id"))
|
||||
provider = _get_provider(prm["provider_id"] if "provider_id" in prm.keys() else None)
|
||||
model = prm["llm_model"] or get_setting("llm_model", _SEED_MODEL)
|
||||
num_ctx_setting = prm["llm_num_ctx"] or get_setting("llm_num_ctx", "auto")
|
||||
num_predict = int(prm["llm_num_predict"] or get_setting("llm_num_predict", str(_SEED_NUM_PREDICT)))
|
||||
@@ -795,17 +795,46 @@ def settings_page(msg: str = ""):
|
||||
email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall()
|
||||
|
||||
provider_rows = "".join(
|
||||
f"""<tr>
|
||||
<td>{p["name"]}</td>
|
||||
<td><code class='small'>{p["base_url"]}</code></td>
|
||||
<td class='text-center'><i class='bi bi-{'key-fill text-success' if p["api_key"] else 'key text-secondary'}'></i></td>
|
||||
<td class='text-center'>{'<span class=\'badge bg-primary\'>Standard</span>' if p["is_default"] else f'<form method=\'post\' action=\'/providers/{p["id"]}/set-default\' class=\'d-inline\'><button class=\'btn btn-sm btn-outline-secondary py-0 px-2\'>Standard</button></form>'}</td>
|
||||
<td class='text-end'>
|
||||
f"""<tr id='prow_{p["id"]}'>
|
||||
<td class='align-middle'><span class='fw-semibold'>{p["name"]}</span></td>
|
||||
<td class='align-middle'><code class='small'>{p["base_url"]}</code></td>
|
||||
<td class='text-center align-middle'><i class='bi bi-{"key-fill text-success" if p["api_key"] else "key text-secondary"}' title='{"API-Key gesetzt" if p["api_key"] else "Kein API-Key"}'></i></td>
|
||||
<td class='text-center align-middle'>
|
||||
{"<span class='badge bg-primary'>Standard</span>" if p["is_default"] else f"<form method='post' action='/providers/{p['id']}/set-default' class='d-inline'><button class='btn btn-sm btn-outline-secondary py-0 px-2' title='Als Standard setzen'><i class='bi bi-star'></i></button></form>"}
|
||||
</td>
|
||||
<td class='text-end align-middle'>
|
||||
<button class='btn btn-sm btn-outline-primary py-0 px-2 me-1' type='button'
|
||||
onclick='toggleEdit({p["id"]})' title='Bearbeiten'><i class='bi bi-pencil'></i></button>
|
||||
<form method='post' action='/providers/{p["id"]}/delete' class='d-inline'
|
||||
onsubmit='return confirm("Anbieter löschen?")'>
|
||||
<button class='btn btn-sm btn-outline-danger py-0 px-2'><i class='bi bi-trash'></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id='pedit_{p["id"]}' class='d-none bg-body-tertiary'>
|
||||
<td colspan='5' class='p-3'>
|
||||
<form method='post' action='/providers/{p["id"]}/update'>
|
||||
<div class='row g-2 align-items-end'>
|
||||
<div class='col-12 col-md-3'>
|
||||
<label class='form-label small mb-1'>Name</label>
|
||||
<input class='form-control form-control-sm' name='name' value='{p["name"]}' required>
|
||||
</div>
|
||||
<div class='col-12 col-md-4'>
|
||||
<label class='form-label small mb-1'>Base-URL</label>
|
||||
<input class='form-control form-control-sm font-monospace' name='base_url' value='{p["base_url"]}' required>
|
||||
</div>
|
||||
<div class='col-12 col-md-3'>
|
||||
<label class='form-label small mb-1'>API-Key <span class='text-secondary'>(leer lassen = unverändert)</span></label>
|
||||
<input class='form-control form-control-sm' type='password' name='api_key' placeholder='{"••••••••" if p["api_key"] else "kein Key"}' autocomplete='new-password'>
|
||||
</div>
|
||||
<div class='col-auto d-flex gap-2 align-items-end'>
|
||||
<button class='btn btn-primary btn-sm' type='submit'><i class='bi bi-check2'></i> Speichern</button>
|
||||
<button class='btn btn-outline-secondary btn-sm' type='button' onclick='toggleEdit({p["id"]})'><i class='bi bi-x'></i></button>
|
||||
{"<form method='post' action='/providers/" + str(p['id']) + "/clear-key' class='d-inline'><button class='btn btn-outline-warning btn-sm' type='submit' title='API-Key löschen'><i class='bi bi-key-x'></i></button></form>" if p["api_key"] else ""}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>"""
|
||||
for p in providers
|
||||
)
|
||||
@@ -842,9 +871,14 @@ def settings_page(msg: str = ""):
|
||||
{f"<div class='alert alert-success py-2'>{msg}</div>" if msg else ""}
|
||||
|
||||
<div class='card' id='anbieter'>
|
||||
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-hdd-network text-primary me-1'></i> LLM-Anbieter</h5>
|
||||
<p class='text-secondary small mb-2'>OpenAI-kompatible Endpunkte (Ollama, OpenAI, Groq, LM Studio, …)</p>
|
||||
{"<table class='table table-sm mb-3'><thead><tr><th>Name</th><th>Base-URL</th><th class='text-center'>API-Key</th><th class='text-center'>Standard</th><th></th></tr></thead><tbody>" + provider_rows + "</tbody></table>" if providers else "<p class='text-secondary small'>Noch kein Anbieter konfiguriert.</p>"}
|
||||
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-hdd-network text-primary me-1'></i> LLM-Anbieter & Standard-Modell</h5>
|
||||
|
||||
<h6 class='small text-secondary fw-semibold mb-2'>SCHRITT 1 — Anbieter verwalten</h6>
|
||||
<p class='text-secondary small mb-2'>OpenAI-kompatible Endpunkte (Ollama, OpenAI, Groq, LM Studio, …). <i class='bi bi-star'></i> = als Standard setzen.</p>
|
||||
{"<div class='table-responsive'><table class='table table-sm mb-3'><thead><tr><th>Name</th><th>Base-URL</th><th class='text-center'>API-Key</th><th class='text-center'><i class='bi bi-star' title='Standard'></i></th><th></th></tr></thead><tbody>" + provider_rows + "</tbody></table></div>" if providers else "<p class='text-secondary small'>Noch kein Anbieter konfiguriert.</p>"}
|
||||
<details class='mb-4'>
|
||||
<summary class='text-secondary small' style='cursor:pointer'><i class='bi bi-plus-circle me-1'></i> Neuen Anbieter hinzufügen</summary>
|
||||
<div class='mt-2'>
|
||||
<form method='post' action='/providers/add' class='row g-2 align-items-end'>
|
||||
<div class='col-12 col-md-3'>
|
||||
<label class='form-label'>Name</label>
|
||||
@@ -861,23 +895,28 @@ def settings_page(msg: str = ""):
|
||||
<div class='col-auto d-flex align-items-end gap-2'>
|
||||
<div class='form-check mb-2'>
|
||||
<input class='form-check-input' type='checkbox' name='is_default' value='1' id='provDefault'>
|
||||
<label class='form-check-label' for='provDefault'>Standard</label>
|
||||
<label class='form-check-label' for='provDefault'>Als Standard</label>
|
||||
</div>
|
||||
<button class='btn btn-outline-primary mb-0' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class='card mt-3'>
|
||||
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-sliders text-primary me-1'></i> Standard-LLM-Parameter</h5>
|
||||
<p class='text-secondary small mb-2'>Gelten für alle Prompts ohne eigene Einstellungen. Anbieter = markierter Standard-Anbieter oben.</p>
|
||||
<hr class='my-3'>
|
||||
<h6 class='small text-secondary fw-semibold mb-2'>SCHRITT 2 — Standard-Modell & Parameter festlegen</h6>
|
||||
<p class='text-secondary small mb-3'>
|
||||
Gilt für alle Prompts ohne eigene Einstellungen. Standard-Anbieter:
|
||||
<strong>{next((p["name"] for p in providers if p["is_default"]), "– keiner gesetzt –")}</strong>
|
||||
</p>
|
||||
<form method='post' action='/settings'>
|
||||
<div class='row g-3'>
|
||||
|
||||
<div class='col-12'>
|
||||
<label class='form-label'>Standard-Modell</label>
|
||||
<div class='input-group'>
|
||||
<select id='providerSelect' class='form-select' style='max-width:220px' onchange='updateProviderForLoad()'>
|
||||
<select id='providerSelect' class='form-select flex-grow-0' style='width:auto;max-width:200px'
|
||||
title='Anbieter für Modellliste wählen' onchange='onProviderChange()'>
|
||||
{provider_id_opts}
|
||||
</select>
|
||||
<input id='modelInput' class='form-control font-monospace' name='llm_model'
|
||||
@@ -889,7 +928,7 @@ def settings_page(msg: str = ""):
|
||||
<div id='modelSelectWrap' class='mt-1 d-none'>
|
||||
<select id='modelSelect' class='form-select font-monospace'></select>
|
||||
</div>
|
||||
<div class='form-text'>Aktuell gespeichert: <code>{current_model}</code></div>
|
||||
<div class='form-text'>Anbieter im Dropdown links wählen → <em>Modelle laden</em> → Modell auswählen → Speichern. Aktuell: <code>{current_model}</code></div>
|
||||
</div>
|
||||
|
||||
<div class='col-12 col-md-4'>
|
||||
@@ -990,21 +1029,24 @@ def settings_page(msg: str = ""):
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let _loadProviderId = null;
|
||||
function updateProviderForLoad() {{
|
||||
_loadProviderId = document.getElementById('providerSelect')?.value || null;
|
||||
function onProviderChange() {{
|
||||
document.getElementById('modelSelectWrap').classList.add('d-none');
|
||||
}}
|
||||
|
||||
function toggleEdit(pid) {{
|
||||
const row = document.getElementById('pedit_' + pid);
|
||||
row.classList.toggle('d-none');
|
||||
}}
|
||||
|
||||
async function loadModels() {{
|
||||
const btn = document.getElementById('loadModelsBtn');
|
||||
const icon = document.getElementById('loadIcon');
|
||||
const wrap = document.getElementById('modelSelectWrap');
|
||||
const sel = document.getElementById('modelSelect');
|
||||
const inp = document.getElementById('modelInput');
|
||||
const pid = document.getElementById('providerSelect')?.value || '';
|
||||
btn.disabled = true;
|
||||
icon.className = 'bi bi-hourglass-split';
|
||||
const pid = _loadProviderId || document.getElementById('providerSelect')?.value || '';
|
||||
try {{
|
||||
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
|
||||
const data = await res.json();
|
||||
@@ -1118,6 +1160,34 @@ def set_default_provider(provider_id: int):
|
||||
return RedirectResponse("/settings?msg=Standard-Anbieter+gesetzt.#anbieter", status_code=303)
|
||||
|
||||
|
||||
@app.post("/providers/{provider_id}/update", response_class=HTMLResponse)
|
||||
def update_provider(
|
||||
provider_id: int,
|
||||
name: str = Form(...),
|
||||
base_url: str = Form(...),
|
||||
api_key: str = Form(""),
|
||||
):
|
||||
with db() as c:
|
||||
if api_key.strip():
|
||||
c.execute(
|
||||
"UPDATE providers SET name=?, base_url=?, api_key=? WHERE id=?",
|
||||
(name.strip(), base_url.strip().rstrip("/"), api_key.strip(), provider_id),
|
||||
)
|
||||
else:
|
||||
c.execute(
|
||||
"UPDATE providers SET name=?, base_url=? WHERE id=?",
|
||||
(name.strip(), base_url.strip().rstrip("/"), provider_id),
|
||||
)
|
||||
return RedirectResponse("/settings?msg=Anbieter+aktualisiert.#anbieter", status_code=303)
|
||||
|
||||
|
||||
@app.post("/providers/{provider_id}/clear-key", response_class=HTMLResponse)
|
||||
def clear_provider_key(provider_id: int):
|
||||
with db() as c:
|
||||
c.execute("UPDATE providers SET api_key=NULL WHERE id=?", (provider_id,))
|
||||
return RedirectResponse("/settings?msg=API-Key+gelöscht.#anbieter", status_code=303)
|
||||
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def upload_page(msg: str = ""):
|
||||
projects = get_projects()
|
||||
@@ -1775,7 +1845,7 @@ def _prompt_llm_block(p, providers, def_model, def_num_ctx, def_num_predict, def
|
||||
np_ = p["llm_num_predict"] or def_num_predict
|
||||
rp = p["llm_repeat_penalty"] or def_repeat_penalty
|
||||
rn = p["llm_repeat_last_n"] or def_repeat_last_n
|
||||
cur_provider_id = p.get("provider_id")
|
||||
cur_provider_id = p["provider_id"] if "provider_id" in p.keys() else None
|
||||
disabled = "disabled" if use_default else ""
|
||||
opacity = "opacity:.45;pointer-events:none" if use_default else ""
|
||||
ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"),
|
||||
|
||||
Reference in New Issue
Block a user