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_penalty = float(get_setting("llm_repeat_penalty", "1.15"))
|
||||||
repeat_last_n = int(get_setting("llm_repeat_last_n", "128"))
|
repeat_last_n = int(get_setting("llm_repeat_last_n", "128"))
|
||||||
else:
|
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)
|
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_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)))
|
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()
|
email_addrs = c.execute("SELECT id, label, email FROM email_addresses ORDER BY label").fetchall()
|
||||||
|
|
||||||
provider_rows = "".join(
|
provider_rows = "".join(
|
||||||
f"""<tr>
|
f"""<tr id='prow_{p["id"]}'>
|
||||||
<td>{p["name"]}</td>
|
<td class='align-middle'><span class='fw-semibold'>{p["name"]}</span></td>
|
||||||
<td><code class='small'>{p["base_url"]}</code></td>
|
<td class='align-middle'><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 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'>{'<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-center align-middle'>
|
||||||
<td class='text-end'>
|
{"<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'
|
<form method='post' action='/providers/{p["id"]}/delete' class='d-inline'
|
||||||
onsubmit='return confirm("Anbieter löschen?")'>
|
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>
|
<button class='btn btn-sm btn-outline-danger py-0 px-2'><i class='bi bi-trash'></i></button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</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>"""
|
</tr>"""
|
||||||
for p in providers
|
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 ""}
|
{f"<div class='alert alert-success py-2'>{msg}</div>" if msg else ""}
|
||||||
|
|
||||||
<div class='card' id='anbieter'>
|
<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>
|
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-hdd-network text-primary me-1'></i> LLM-Anbieter & Standard-Modell</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>"}
|
<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'>
|
<form method='post' action='/providers/add' class='row g-2 align-items-end'>
|
||||||
<div class='col-12 col-md-3'>
|
<div class='col-12 col-md-3'>
|
||||||
<label class='form-label'>Name</label>
|
<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='col-auto d-flex align-items-end gap-2'>
|
||||||
<div class='form-check mb-2'>
|
<div class='form-check mb-2'>
|
||||||
<input class='form-check-input' type='checkbox' name='is_default' value='1' id='provDefault'>
|
<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>
|
</div>
|
||||||
<button class='btn btn-outline-primary mb-0' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button>
|
<button class='btn btn-outline-primary mb-0' type='submit'><i class='bi bi-plus-lg'></i> Hinzufügen</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<div class='card mt-3'>
|
<hr class='my-3'>
|
||||||
<h5 class='h6 fw-semibold mb-3'><i class='bi bi-sliders text-primary me-1'></i> Standard-LLM-Parameter</h5>
|
<h6 class='small text-secondary fw-semibold mb-2'>SCHRITT 2 — Standard-Modell & Parameter festlegen</h6>
|
||||||
<p class='text-secondary small mb-2'>Gelten für alle Prompts ohne eigene Einstellungen. Anbieter = markierter Standard-Anbieter oben.</p>
|
<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'>
|
<form method='post' action='/settings'>
|
||||||
<div class='row g-3'>
|
<div class='row g-3'>
|
||||||
|
|
||||||
<div class='col-12'>
|
<div class='col-12'>
|
||||||
<label class='form-label'>Standard-Modell</label>
|
<label class='form-label'>Standard-Modell</label>
|
||||||
<div class='input-group'>
|
<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}
|
{provider_id_opts}
|
||||||
</select>
|
</select>
|
||||||
<input id='modelInput' class='form-control font-monospace' name='llm_model'
|
<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'>
|
<div id='modelSelectWrap' class='mt-1 d-none'>
|
||||||
<select id='modelSelect' class='form-select font-monospace'></select>
|
<select id='modelSelect' class='form-select font-monospace'></select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class='col-12 col-md-4'>
|
<div class='col-12 col-md-4'>
|
||||||
@@ -990,21 +1029,24 @@ def settings_page(msg: str = ""):
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let _loadProviderId = null;
|
function onProviderChange() {{
|
||||||
function updateProviderForLoad() {{
|
|
||||||
_loadProviderId = document.getElementById('providerSelect')?.value || null;
|
|
||||||
document.getElementById('modelSelectWrap').classList.add('d-none');
|
document.getElementById('modelSelectWrap').classList.add('d-none');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
function toggleEdit(pid) {{
|
||||||
|
const row = document.getElementById('pedit_' + pid);
|
||||||
|
row.classList.toggle('d-none');
|
||||||
|
}}
|
||||||
|
|
||||||
async function loadModels() {{
|
async function loadModels() {{
|
||||||
const btn = document.getElementById('loadModelsBtn');
|
const btn = document.getElementById('loadModelsBtn');
|
||||||
const icon = document.getElementById('loadIcon');
|
const icon = document.getElementById('loadIcon');
|
||||||
const wrap = document.getElementById('modelSelectWrap');
|
const wrap = document.getElementById('modelSelectWrap');
|
||||||
const sel = document.getElementById('modelSelect');
|
const sel = document.getElementById('modelSelect');
|
||||||
const inp = document.getElementById('modelInput');
|
const inp = document.getElementById('modelInput');
|
||||||
|
const pid = document.getElementById('providerSelect')?.value || '';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
icon.className = 'bi bi-hourglass-split';
|
icon.className = 'bi bi-hourglass-split';
|
||||||
const pid = _loadProviderId || document.getElementById('providerSelect')?.value || '';
|
|
||||||
try {{
|
try {{
|
||||||
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
|
const res = await fetch('/settings/models' + (pid ? '?provider_id=' + pid : ''));
|
||||||
const data = await res.json();
|
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)
|
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)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
def upload_page(msg: str = ""):
|
def upload_page(msg: str = ""):
|
||||||
projects = get_projects()
|
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
|
np_ = p["llm_num_predict"] or def_num_predict
|
||||||
rp = p["llm_repeat_penalty"] or def_repeat_penalty
|
rp = p["llm_repeat_penalty"] or def_repeat_penalty
|
||||||
rn = p["llm_repeat_last_n"] or def_repeat_last_n
|
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 ""
|
disabled = "disabled" if use_default else ""
|
||||||
opacity = "opacity:.45;pointer-events:none" 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"),
|
ctx_opts = [("auto","Automatisch"), ("4096","4 096"), ("8192","8 192"), ("16384","16 384"),
|
||||||
|
|||||||
Reference in New Issue
Block a user