fix(ux): Idiotensicherheit — 8 Audit-Fixes
Behebung der drei kritischsten UX-Schwachstellen aus dem Produkt-Audit:
1. ConnectPanel: Steps 2-4 (Modellauswahl, Tool-Picker, Config-Snippet,
Vision) werden blockiert wenn noch kein Modell installiert ist.
Stattdessen klarer Hinweis "Erst im Cookbook ein Modell holen" mit
direktem CTA. Verhindert dass Anfänger ein Snippet mit Fake-"coder"
kopieren, bevor ein echtes Modell existiert.
2. OverviewPanel: preload() schluckte Fehler still (catch {}). Zeigt
jetzt einen verständlichen Toast wenn die Engine nicht erreichbar ist.
Schnellstart ist kontextsensitiv: ohne Modelle zeigt er Onboarding-
Schritte (Modell holen → Verbinden → Guide lesen) statt sinnloser
Aktionen wie "Speicher freigeben".
3. CookbookPanel Profi-Suche: doDownload() nutzte zwei sequenzielle
API-Calls (download + register) — wenn der zweite scheiterte, blieb
das System in einem inkonsistenten Halbzustand. Jetzt ein einziger
atomarer Aufruf an /api/cookbook/install-model (wie Recipe/Discover).
Backend-Erweiterung: InstallModelReq akzeptiert jetzt 'file' und
'ctx' um Nutzer-Auswahl direkt zu übergeben.
4. Cookbook Section 1 als "Hier starten" markiert ("1 · Anwendungsfall
wählen & installieren" mit erklärendem Sub-Text).
5. Post-Download-Toasts: Alle drei Install-Pfade sagen nun einheitlich
"Modell erscheint danach automatisch im Modelle-Tab" statt nur
"siehe Aktivität".
6. too_tight-Alternative: Bei Modellen/Setups die den Speicher übersteigen
wird jetzt eine konkrete Alternative (kleinere Datei / passendes Rezept)
verlinkt statt nur "Trotzdem installieren".
7. Duplikat extract_params_b aus cookbook.py entfernt — importiert jetzt
aus hw_math (Single Source of Truth).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -115,8 +115,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Model selector -->
|
||||
<!-- Steps 2–4: nur anzeigen wenn Modelle installiert sind -->
|
||||
{#if allModels.length}
|
||||
|
||||
<!-- Step 2: Model selector -->
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>2 · Modelle auswählen</h3>
|
||||
@@ -140,90 +142,104 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Step 3: Tool picker -->
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>3 · Tool wählen</h3></div>
|
||||
<div class="card-sub">Für welches Werkzeug brauchst du die Konfiguration?</div>
|
||||
<div class="grid grid-4" style="gap:8px">
|
||||
{#each [
|
||||
['zed', 'Zed', 'Schneller nativer Editor — Empfohlen'],
|
||||
['opencode', 'OpenCode', 'Open-Source Coding-Agent (Terminal/Desktop)'],
|
||||
['cline', 'Cline / Cursor', 'VS Code Extension, Bring-Your-Own-Key'],
|
||||
['openwebui', 'OpenWebUI', 'Browser-Chat-Interface']
|
||||
] as [id, label, desc]}
|
||||
<button class="card-btn{selectedTool === id ? ' cb-best' : ''}" style="text-align:left;padding:12px 16px" onclick={() => selectedTool = id}>
|
||||
<div style="font-weight:500;font-size:13.5px">{label}</div>
|
||||
<div class="card-sub" style="margin:3px 0 0">{desc}</div>
|
||||
</button>
|
||||
{/each}
|
||||
<!-- Step 3: Tool picker -->
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>3 · Tool wählen</h3></div>
|
||||
<div class="card-sub">Für welches Werkzeug brauchst du die Konfiguration?</div>
|
||||
<div class="grid grid-4" style="gap:8px">
|
||||
{#each [
|
||||
['zed', 'Zed', 'Schneller nativer Editor — Empfohlen'],
|
||||
['opencode', 'OpenCode', 'Open-Source Coding-Agent (Terminal/Desktop)'],
|
||||
['cline', 'Cline / Cursor', 'VS Code Extension, Bring-Your-Own-Key'],
|
||||
['openwebui', 'OpenWebUI', 'Browser-Chat-Interface']
|
||||
] as [id, label, desc]}
|
||||
<button class="card-btn{selectedTool === id ? ' cb-best' : ''}" style="text-align:left;padding:12px 16px" onclick={() => selectedTool = id}>
|
||||
<div style="font-weight:500;font-size:13.5px">{label}</div>
|
||||
<div class="card-sub" style="margin:3px 0 0">{desc}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 4: Config snippet -->
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>4 · Config-Snippet</h3></div>
|
||||
{#if selectedTool === 'zed'}
|
||||
{@const block = zedBlock(url, activeIds)}
|
||||
<p>Öffne die Zed-Settings mit <code>Cmd/Strg + ,</code> und <b>füge diesen Block</b> in deine bestehende <code>settings.json</code> ein. Falls bereits ein <code>"bosgame"</code>-Eintrag existiert: vorher löschen.</p>
|
||||
<div class="alert warn" style="margin-bottom:12px;padding:10px 14px"><span class="a-dot"></span><span>Nur diesen Block einfügen, <b>nicht die ganze Datei ersetzen</b> — sonst gehen andere Einstellungen verloren.</span></div>
|
||||
<div style="position:relative">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{block}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px"><b>API-Key:</b> Öffne den Agent-Panel → Anbieter <b>bosgame</b> → trag irgendeinen Wert ein (z.B. <code>local</code>).</p>
|
||||
<div class="hint">Danach im Agent-Panel das Modell <b>bosgame / {firstModel}</b> auswählen.</div>
|
||||
{:else if selectedTool === 'opencode'}
|
||||
{@const block = openCodeBlock(url, activeIds)}
|
||||
<p>Datei anlegen oder öffnen:</p>
|
||||
<p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\.config\opencode\opencode.json</code></p>
|
||||
<div class="alert warn" style="margin:8px 0 12px;padding:10px 14px"><span class="a-dot"></span><span>Nur den <code>"provider"</code>-Block einfügen. Falls bereits ein <code>"llama-swap"</code>-Eintrag vorhanden: <b>vorher löschen</b>, sonst erscheinen Modelle doppelt.</span></div>
|
||||
<div style="position:relative">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{block}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px">Danach in OpenCode <code>/models</code> → <b>llama-swap/{firstModel}</b> wählen.</p>
|
||||
{:else if selectedTool === 'cline'}
|
||||
<p>In Cline/Cursor den Provider <b>„OpenAI Compatible"</b> wählen:</p>
|
||||
<label>Basis-URL</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" readonly value={url} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
<label>Modell-ID</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" readonly value={firstModel} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
<div class="hint">API-Key: ein beliebiger Wert (z.B. <code>local</code>).</div>
|
||||
{:else if selectedTool === 'openwebui'}
|
||||
<p>Settings → Admin Panel → Connections → neue OpenAI-Verbindung:</p>
|
||||
<label>Basis-URL</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" readonly value={url} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
<label>API-Key</label>
|
||||
<div class="flex gap-2" style="align-items:stretch">
|
||||
<input class="mono-sm" readonly value="dummy-key" style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
<!-- Step 4: Config snippet -->
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>4 · Config-Snippet</h3></div>
|
||||
{#if selectedTool === 'zed'}
|
||||
{@const block = zedBlock(url, activeIds)}
|
||||
<p>Öffne die Zed-Settings mit <code>Cmd/Strg + ,</code> und <b>füge diesen Block</b> in deine bestehende <code>settings.json</code> ein. Falls bereits ein <code>"bosgame"</code>-Eintrag existiert: vorher löschen.</p>
|
||||
<div class="alert warn" style="margin-bottom:12px;padding:10px 14px"><span class="a-dot"></span><span>Nur diesen Block einfügen, <b>nicht die ganze Datei ersetzen</b> — sonst gehen andere Einstellungen verloren.</span></div>
|
||||
<div style="position:relative">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{block}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px"><b>API-Key:</b> Öffne den Agent-Panel → Anbieter <b>bosgame</b> → trag irgendeinen Wert ein (z.B. <code>local</code>).</p>
|
||||
<div class="hint">Danach im Agent-Panel das Modell <b>bosgame / {firstModel}</b> auswählen.</div>
|
||||
{:else if selectedTool === 'opencode'}
|
||||
{@const block = openCodeBlock(url, activeIds)}
|
||||
<p>Datei anlegen oder öffnen:</p>
|
||||
<p class="mono-sm" style="line-height:1.6">Windows: <code>%USERPROFILE%\.config\opencode\opencode.json</code></p>
|
||||
<div class="alert warn" style="margin:8px 0 12px;padding:10px 14px"><span class="a-dot"></span><span>Nur den <code>"provider"</code>-Block einfügen. Falls bereits ein <code>"llama-swap"</code>-Eintrag vorhanden: <b>vorher löschen</b>, sonst erscheinen Modelle doppelt.</span></div>
|
||||
<div style="position:relative">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{block}</code></div>
|
||||
</div>
|
||||
<p style="margin-top:12px">Danach in OpenCode <code>/models</code> → <b>llama-swap/{firstModel}</b> wählen.</p>
|
||||
{:else if selectedTool === 'cline'}
|
||||
<p>In Cline/Cursor den Provider <b>„OpenAI Compatible"</b> wählen:</p>
|
||||
<label>Basis-URL</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" readonly value={url} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
<label>Modell-ID</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" readonly value={firstModel} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
<div class="hint">API-Key: ein beliebiger Wert (z.B. <code>local</code>).</div>
|
||||
{:else if selectedTool === 'openwebui'}
|
||||
<p>Settings → Admin Panel → Connections → neue OpenAI-Verbindung:</p>
|
||||
<label>Basis-URL</label>
|
||||
<div class="flex gap-2" style="align-items:stretch;margin-bottom:12px">
|
||||
<input class="mono-sm" readonly value={url} style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
<label>API-Key</label>
|
||||
<div class="flex gap-2" style="align-items:stretch">
|
||||
<input class="mono-sm" readonly value="dummy-key" style="flex:1;margin:0">
|
||||
<button class="ghost" onclick={e => copyVal((e.currentTarget as HTMLElement).previousElementSibling as HTMLInputElement)}>Kopieren</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Vision -->
|
||||
{#if visionModel}
|
||||
{@const snippet = `curl ${url}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${visionModel}",\n "messages": [{"role":"user","content":[{"type":"text","text":"Was ist auf dem Bild falsch?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}]}]\n}'`}
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Bilder einbinden (Vision)</h3>
|
||||
<span class="chip" style="background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a">{@html icon('check')} „{visionModel}" bereit</span>
|
||||
</div>
|
||||
<div class="card-sub">Mit Vision-Modellen kannst du Screenshots oder Fehlerbilder direkt mitschicken.</div>
|
||||
<div style="position:relative;margin-top:8px">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{snippet}</code></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Vision -->
|
||||
{#if visionModel}
|
||||
{@const snippet = `curl ${url}/chat/completions \\\n -H "Content-Type: application/json" \\\n -d '{\n "model": "${visionModel}",\n "messages": [{"role":"user","content":[{"type":"text","text":"Was ist auf dem Bild falsch?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,<DEIN_BILD>"}}]}]\n}'`}
|
||||
{:else}
|
||||
<!-- Kein Modell installiert: Schritt 0 erklären statt leeres Snippet ausgeben -->
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Bilder einbinden (Vision)</h3>
|
||||
<span class="chip" style="background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a">{@html icon('check')} „{visionModel}" bereit</span>
|
||||
</div>
|
||||
<div class="card-sub">Mit Vision-Modellen kannst du Screenshots oder Fehlerbilder direkt mitschicken.</div>
|
||||
<div style="position:relative;margin-top:8px">
|
||||
<button class="ghost" style="position:absolute;top:8px;right:8px;z-index:1" onclick={e => copyCode(e.currentTarget!.parentElement!)}>Kopieren</button>
|
||||
<div class="log" style="max-height:none"><code>{snippet}</code></div>
|
||||
<div class="card-h"><h3>2 · Erst ein Modell installieren</h3></div>
|
||||
<div class="empty-c" style="padding:8px 0 4px">
|
||||
<div class="e-t">Noch kein Modell eingerichtet</div>
|
||||
<div class="e-s">Bevor du ein Tool verbinden kannst, braucht die Engine mindestens ein Modell. Das Config-Snippet wird dann automatisch mit deinen echten Modellnamen befüllt.</div>
|
||||
<button class="primary" style="margin-top:14px"
|
||||
onclick={() => document.querySelector(".nav-item[data-view='cookbook']")?.dispatchEvent(new MouseEvent('click'))}>
|
||||
→ Zum Cookbook (Modell holen)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -163,7 +163,7 @@
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Starte…' }
|
||||
try {
|
||||
const r = await api('/api/cookbook/install-recipe', { method: 'POST', body: JSON.stringify({ recipe_id: id, hf_token: getHfToken() }) })
|
||||
toast(`${r.count} Downloads gestartet — siehe Aktivität.`)
|
||||
toast(`${r.count} Download${r.count !== 1 ? 's' : ''} läuft — Modelle erscheinen danach automatisch im Modelle-Tab.`)
|
||||
recipeModal = null; goView('activity')
|
||||
} catch (e: any) { toast(e.message, true); if (btn) { btn.disabled = false; btn.textContent = 'Installieren' } }
|
||||
}
|
||||
@@ -196,9 +196,14 @@
|
||||
if (!modelModalRepo || !modelFile) return toast('Bitte eine GGUF-Datei wählen.', true)
|
||||
modelLoading = true
|
||||
try {
|
||||
const res = await api('/api/download', { method: 'POST', body: JSON.stringify({ repo: modelModalRepo, file: modelFile, hf_token: getHfToken() }) })
|
||||
await api('/api/register', { method: 'POST', body: JSON.stringify({ role: modelRole, model_path: res.expected_path, ctx: modelCtx }) })
|
||||
toast('Download gestartet — siehe Aktivität.'); modelModal = false; goView('activity')
|
||||
const params_b = modelAnalysis?.params_b ?? 7.0
|
||||
const quant = modelFit?.quant ?? 'Q4_K_M'
|
||||
await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({
|
||||
repo: modelModalRepo, role: modelRole, params_b, quant,
|
||||
file: modelFile, ctx: modelCtx, hf_token: getHfToken()
|
||||
})})
|
||||
toast('Download läuft — Modell erscheint danach automatisch im Modelle-Tab.')
|
||||
modelModal = false; goView('activity')
|
||||
} catch (e: any) { toast('Fehler: ' + e.message, true) }
|
||||
modelLoading = false
|
||||
}
|
||||
@@ -208,7 +213,7 @@
|
||||
btnEl.disabled = true; btnEl.textContent = 'Starte…'
|
||||
try {
|
||||
await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({ repo, role, params_b, quant: 'Q4_K_M', hf_token: getHfToken() }) })
|
||||
toast('Download gestartet — siehe Aktivität.'); goView('activity')
|
||||
toast('Download läuft — Modell erscheint danach automatisch im Modelle-Tab.'); goView('activity')
|
||||
} catch (e: any) { toast(e.message, true); btnEl.disabled = false; btnEl.textContent = 'Installieren' }
|
||||
}
|
||||
|
||||
@@ -263,10 +268,10 @@
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h">
|
||||
<h3>Use-Case-Setups</h3>
|
||||
<h3>1 · Anwendungsfall wählen & installieren</h3>
|
||||
<button class="ghost" style="margin-left:auto" onclick={openNewRecipe}>+ Eigenes Setup</button>
|
||||
</div>
|
||||
<div class="card-sub">Fertige Modell-Bundles für deinen Anwendungsfall. Hardware-Ampel zeigt, ob es auf deinen PC passt.</div>
|
||||
<div class="card-sub">Hier starten: Fertige Modell-Bundles für deinen Anwendungsfall — ein Klick installiert alles. Die Hardware-Ampel zeigt vorab, ob das Setup auf deinen PC passt.</div>
|
||||
{#if recipeLoading}
|
||||
<div class="empty" style="text-align:center;padding:30px">Lade Setups…</div>
|
||||
{:else}
|
||||
@@ -450,9 +455,21 @@
|
||||
{@const maxRam = Math.max(...r.models.map((m: any) => m.fit?.req_gb ?? 0))}
|
||||
<div class="hint" style="margin:12px 0">Größter Spitzenbedarf: <b>~{maxRam.toFixed(1)} GB</b>. Es läuft immer nur <b>ein</b> Modell gleichzeitig — das größte bestimmt, ob das Setup passt.</div>
|
||||
{/if}
|
||||
{#if r.fit_level === 'too_tight'}
|
||||
{@const smallerRec = recipes.find((x: any) => !x.user && x.fit_level !== 'too_tight' && x.id !== r.id)}
|
||||
<div class="hint" style="color:var(--warn);margin-bottom:10px;border-left:2px solid var(--warn);padding-left:10px">
|
||||
⚠ Dieses Setup übersteigt deinen verfügbaren Speicher.
|
||||
{#if smallerRec}
|
||||
Alternative: <a href="#" style="color:var(--accent)"
|
||||
onclick={e => { e.preventDefault(); recipeModal = null; setTimeout(() => openRecipe(smallerRec.id), 50) }}>
|
||||
„{smallerRec.title}" ansehen
|
||||
</a> — passt auf dein System.
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<button id="cb-r-install-btn" class="primary{r.fit_level === 'too_tight' ? ' warn' : ''}" style="width:100%"
|
||||
onclick={() => installRecipe(r.id)}>
|
||||
{r.fit_level === 'too_tight' ? 'Trotzdem installieren (zu groß)' : 'Komplettes Setup installieren'}
|
||||
{r.fit_level === 'too_tight' ? 'Trotzdem installieren (nicht empfohlen)' : 'Komplettes Setup installieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -501,9 +518,24 @@
|
||||
<span class="fit-badge {fitCls(modelFit.fit.level)}">{modelFit.fit.text}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if modelFit?.fit.level === 'too_tight' && modelAnalysis?.files}
|
||||
{@const better = modelAnalysis.files.find((f: any) => f.fit.level !== 'too_tight' && f.filename !== modelFile)}
|
||||
{#if better}
|
||||
<div class="hint" style="color:var(--warn);margin:-8px 0 14px;border-left:2px solid var(--warn);padding-left:10px">
|
||||
⚠ Diese Datei ist zu groß für deinen Speicher. Kleinere Alternative:
|
||||
<a href="#" style="color:var(--accent)" onclick={e => { e.preventDefault(); modelFile = better.filename; updateModelFit() }}>
|
||||
{better.filename} ({fitWord(better.fit.level)})
|
||||
</a> — einfach auswählen.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="hint" style="color:var(--warn);margin:-8px 0 14px;border-left:2px solid var(--warn);padding-left:10px">
|
||||
⚠ Alle Varianten dieses Modells sind zu groß für deinen Speicher. Ein kleineres Modell aus den Empfehlungen wäre die bessere Wahl.
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<button class="primary{modelFit?.fit.level === 'too_tight' ? ' warn' : ''}" style="width:100%" disabled={modelLoading}
|
||||
onclick={doDownload}>
|
||||
{modelLoading ? 'Starte…' : modelFit?.fit.level === 'too_tight' ? 'Trotzdem holen (zu groß)' : 'Herunterladen & Einpflegen'}
|
||||
{modelLoading ? 'Starte…' : modelFit?.fit.level === 'too_tight' ? 'Trotzdem holen (zu groß — nicht empfohlen)' : 'Herunterladen & Einpflegen'}
|
||||
</button>
|
||||
{:else if !modelLoading}
|
||||
<div class="hint">Keine GGUF-Dateien im Repo gefunden.</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { statusStore } from '../stores/status.svelte.js'
|
||||
import { systemStore } from '../stores/system.svelte.js'
|
||||
import { api } from '@core/api.js'
|
||||
import { confirmModal, icon, fmtBytes } from '@core/ui.js'
|
||||
import { confirmModal, toast, icon, fmtBytes } from '@core/ui.js'
|
||||
|
||||
const s = $derived(statusStore.value)
|
||||
const sys = $derived(systemStore.value)
|
||||
@@ -53,7 +53,7 @@
|
||||
async function preload(modelName: string) {
|
||||
preloadingModel = modelName
|
||||
try { await api('/api/preload?model=' + encodeURIComponent(modelName), { method: 'POST' }) }
|
||||
catch {}
|
||||
catch (e: any) { toast('Laden fehlgeschlagen: ' + (e.message || 'Engine nicht erreichbar?'), true) }
|
||||
preloadingModel = null
|
||||
refresh()
|
||||
}
|
||||
@@ -153,27 +153,48 @@
|
||||
|
||||
<div class="card">
|
||||
<div class="card-h"><h3>Schnellstart</h3></div>
|
||||
<div class="card-sub">Die häufigsten Aufgaben — ein Klick.</div>
|
||||
<button class="qa" onclick={() => go('cookbook')}>
|
||||
<span class="qa-ic teal">{@html icon('search')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Modell finden</span><span class="qa-s">Passend zu deiner Hardware</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={freeMemory}>
|
||||
<span class="qa-ic amber">{@html icon('refresh')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Speicher freigeben</span><span class="qa-s">Alle Modelle entladen</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('server')}>
|
||||
<span class="qa-ic blue">{@html icon('file')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Logs ansehen</span><span class="qa-s">Live mitlesen, was läuft</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('connect')}>
|
||||
<span class="qa-ic teal">{@html icon('swap')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Tools verbinden</span><span class="qa-s">Zed, OpenCode, Cline einrichten</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
{#if !configuredModels.length}
|
||||
<!-- Onboarding-Pfad: noch keine Modelle -->
|
||||
<div class="card-sub">Noch kein Modell eingerichtet — starte hier:</div>
|
||||
<button class="qa" onclick={() => go('cookbook')}>
|
||||
<span class="qa-ic teal">{@html icon('search')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Schritt 1 · Modell holen</span><span class="qa-s">Cookbook → passendes Setup installieren</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('connect')} style="opacity:.55;pointer-events:none">
|
||||
<span class="qa-ic teal">{@html icon('swap')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Schritt 2 · Tools verbinden</span><span class="qa-s">Erst nach dem Download verfügbar</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('guides')}>
|
||||
<span class="qa-ic blue">{@html icon('file')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Einsteiger-Guide lesen</span><span class="qa-s">Alle Begriffe erklärt, ohne Vorwissen</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
{:else}
|
||||
<!-- Normalbetrieb -->
|
||||
<div class="card-sub">Die häufigsten Aufgaben — ein Klick.</div>
|
||||
<button class="qa" onclick={() => go('cookbook')}>
|
||||
<span class="qa-ic teal">{@html icon('search')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Modell finden</span><span class="qa-s">Passend zu deiner Hardware</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={freeMemory}>
|
||||
<span class="qa-ic amber">{@html icon('refresh')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Speicher freigeben</span><span class="qa-s">Alle Modelle entladen</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('server')}>
|
||||
<span class="qa-ic blue">{@html icon('file')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Logs ansehen</span><span class="qa-s">Live mitlesen, was läuft</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
<button class="qa" onclick={() => go('connect')}>
|
||||
<span class="qa-ic teal">{@html icon('swap')}</span>
|
||||
<span class="qa-main"><span class="qa-t">Tools verbinden</span><span class="qa-s">Zed, OpenCode, Cline einrichten</span></span>
|
||||
<span class="qa-arrow">{@html icon('chevron')}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user