diff --git a/CLAUDE.md b/CLAUDE.md index b20e5ba..703c6f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,8 +80,10 @@ rsync; **Python-Code-Änderungen brauchen den Restart**. ## Konventionen -- KISS über alles: kein schweres Framework, kein Build-Schritt, solange es ohne geht. -- **SoC ohne Build**: neuer Bereich = ein `routers/.py` (FastAPI-Router) + ein `static/js/panels/.js` (ES-Modul nach Panel-Vertrag) + ein Nav-Eintrag in `index.html`. Gemeinsame Backend-Logik in `config/auth/jobengine/llamaswap`, gemeinsame Frontend-Logik in `js/core/*`. Keine schweren Libs, kein CDN — nur relative `import`s. +- **Frontend: Vite + Svelte 5** (Phase C). Neuer Bereich = `frontend/src/panels/Panel.svelte` + `routers/.py` + `initNav`-Eintrag in `index.html`. Danach `npm run build` in `frontend/` → `static/dist/main.js` wird committet. +- Stores: `frontend/src/stores/status.svelte.ts`, `jobs.svelte.ts`, `system.svelte.ts` — Svelte 5 Runes (`$state`). +- Statische Hilfsfunktionen in `static/js/core/` (api.js, ui.js, nav.js) bleiben als gemeinsame Basis — werden von Vite gebündelt, nicht direkt geladen. +- **Backend SoC**: ein `routers/.py` je Bereich, gemeinsame Logik in `config/auth/jobengine/llamaswap/hw_math`. - Endpoint-URLs bleiben unter `/api/*`; neue Bereiche degradieren sauber, wenn ihre Quelle (sysfs, `amd-smi`, `systemctl`) fehlt (z. B. beim Entwickeln auf Windows). - Funktion darf **nicht** von `localStorage` abhängen (nur das Token-Feld nutzt es, das ist ok). - **Sicherheit**: Das Backend führt Shell-Befehle aus → ausschließlich im vertrauenswürdigen LAN betreiben, niemals offen ins Internet. diff --git a/frontend/src/panels/ConnectPanel.svelte b/frontend/src/panels/ConnectPanel.svelte index 12a2160..1f67604 100644 --- a/frontend/src/panels/ConnectPanel.svelte +++ b/frontend/src/panels/ConnectPanel.svelte @@ -26,7 +26,8 @@ const ms = ids.length ? allModels.filter((m: any) => ids.includes(m.name)) : (allModels.length ? allModels : [{ name: 'coder', meta: {} }]) const entries = ms.map((m: any) => { const img = (m.meta?.caps || []).some((c: string) => /Bild|Vision/i.test(c)) - return ` { "name": "${m.name}", "display_name": "${m.name}", "max_tokens": 32768${img ? `, "capabilities": { "tools": true, "images": true }` : ''} }` + const ctx = m.meta?.ctx || 131072 // nutze konfigurierten Kontext des Modells (Standard: 128k) + return ` { "name": "${m.name}", "display_name": "${m.name}", "max_tokens": ${ctx}${img ? `, "capabilities": { "tools": true, "images": true }` : ''} }` }).join(',\n') return `"language_models": {\n "openai_compatible": {\n "bosgame": {\n "api_url": "${url}",\n "available_models": [\n${entries}\n ]\n }\n }\n}` } diff --git a/frontend/src/panels/ModelsPanel.svelte b/frontend/src/panels/ModelsPanel.svelte index fe5a320..d2077ce 100644 --- a/frontend/src/panels/ModelsPanel.svelte +++ b/frontend/src/panels/ModelsPanel.svelte @@ -35,20 +35,25 @@ chatLoading = false; refresh() } + const DEFAULT_TTL = 300 + // ---- Config Modal ---- let cfgOpen = $state(false) let cfgModel = $state(null) let cfgCtx = $state(8192) + let cfgAlwaysActive = $state(false) function openConfig(name: string) { cfgModel = allModels.find((m: any) => m.name === name) if (!cfgModel) return cfgCtx = cfgModel.meta?.ctx || 8192 + cfgAlwaysActive = (cfgModel.ttl ?? DEFAULT_TTL) >= 99999 cfgOpen = true } async function saveConfig() { + const ttl = cfgAlwaysActive ? 99999 : DEFAULT_TTL try { - await api('/api/update_model', { method: 'POST', body: JSON.stringify({ alias: cfgModel.name, ctx: cfgCtx }) }) + await api('/api/update_model', { method: 'POST', body: JSON.stringify({ alias: cfgModel.name, ctx: cfgCtx, ttl }) }) toast('Gespeichert — aktiv beim nächsten Modell-Start.') cfgOpen = false; refresh() } catch (e: any) { toast(e.message, true) } @@ -218,6 +223,20 @@ {/if}
Es läuft immer nur ein Modell gleichzeitig — ein größerer Kontext hier beeinflusst deine anderen Modelle nicht.
+ +
+
+
Immer aktiv halten
+
Modell bleibt dauerhaft im RAM — nie automatisch entladen. Ideal für den kleinen Scout (~5 GB), der immer schnell antworten soll.
+
+ +
+ diff --git a/hw_math.py b/hw_math.py index 6f5985f..54cfbb8 100644 --- a/hw_math.py +++ b/hw_math.py @@ -36,21 +36,36 @@ def estimate_memory_gb(params_b: float, quant: str, ctx: int) -> float: return weights + context_vram -def estimate_speed(req_gb: float, sys_ram_gb: float) -> float: - """Berechnet die geschätzte Tokens/s basierend auf der 273 GB/s Bandbreite der APU.""" - # Strix Halo hat ca 273 GB/s Unified Memory Bandbreite. +def extract_active_params_b(name: str) -> float | None: + """Aktive Parameter bei MoE-Modellen aus dem Namen (z.B. '30B-A3B' → 3.0). + Gibt None zurück für Dense-Modelle (kein MoE-Suffix erkannt).""" + m = re.search(r'(? float: + """Berechnet die geschätzte Tokens/s basierend auf der 273 GB/s Bandbreite der APU. + moe_active_ratio = aktive_params / gesamt_params; < 1 bei MoE (z.B. 0.1 für 30B-A3B).""" bw = 273 if sys_ram_gb > 8 else 70 if req_gb <= 0: return 0.0 - - # (Bandbreite / Modellgröße) * Effizienz (0.55) raw_tps = (bw / req_gb) * 0.55 + # MoE-Boost: nur ein Bruchteil der Gewichte wird pro Token aktiviert → + # effektiv höhere Durchsatzrate als ein Dense-Modell gleicher Gesamtgröße. + # sqrt-Dämpfung: Routing-Overhead + VRAM-Traffic für alle Experten bleibt. + if moe_active_ratio < 0.8: + raw_tps *= (1.0 / moe_active_ratio) ** 0.5 return raw_tps -def evaluate_fit(params_b: float, quant: str, ctx: int, sys_ram_gb: float) -> dict: - """Berechnet den Fit für ein System mit Shared Memory (APU).""" + +def evaluate_fit(params_b: float, quant: str, ctx: int, sys_ram_gb: float, + name: str = "") -> dict: + """Berechnet den Fit für ein System mit Shared Memory (APU). + name wird für MoE-Erkennung genutzt (optional).""" req_gb = estimate_memory_gb(params_b, quant, ctx) - tps = estimate_speed(req_gb, sys_ram_gb) + active_b = extract_active_params_b(name) if name else None + moe_ratio = (active_b / params_b) if (active_b and params_b > 0) else 1.0 + tps = estimate_speed(req_gb, sys_ram_gb, moe_ratio) # Das OS und andere Prozesse brauchen RAM. Wir lassen 4GB Puffer. usable_ram = max(sys_ram_gb - 4.0, 0) diff --git a/routers/cookbook.py b/routers/cookbook.py index 5500a3d..36fd8a5 100644 --- a/routers/cookbook.py +++ b/routers/cookbook.py @@ -127,7 +127,7 @@ async def analyze_repo(req: AnalyzeRequest): results = [] for f in gguf_files: quant = extract_quant(f) - fit = evaluate_fit(params_b, quant, req.ctx, ram_gb) + fit = evaluate_fit(params_b, quant, req.ctx, ram_gb, name=req.repo_id) # Priority-Score, um den besten Fit an oberste Stelle zu setzen. # "Q4_K_M" ist oft der Sweetspot. @@ -173,7 +173,7 @@ def recipes(): for r in all_recipes(): models, worst = [], "perfect" for m in r["models"]: - fit = evaluate_fit(m["params_b"], m["quant"], 8192, ram_gb) + fit = evaluate_fit(m["params_b"], m["quant"], 8192, ram_gb, name=m.get("name", "")) models.append({**m, "fit": fit, "optimal_ctx": max_ctx_for(m["params_b"], m["quant"], ram_gb)}) if _FIT_ORDER[fit["level"]] > _FIT_ORDER[worst]: worst = fit["level"] @@ -427,7 +427,7 @@ def refresh_discover(ram_gb: float) -> dict: role = _categorize(rid) params_b = extract_params_b(rid) quant = "Q4_K_M" # Referenz-Quant fuer die Fit-Einschaetzung - fit = evaluate_fit(params_b, quant, 8192, ram_gb) + fit = evaluate_fit(params_b, quant, 8192, ram_gb, name=rid) by_cat[role].append({ "name": rid.split("/")[-1], "author": rid.split("/")[0], "repo": rid, "role": role, "params_b": params_b, "quant": quant, @@ -518,7 +518,7 @@ def compute_upgrades(ram_gb): out.append({ "name": up["name"], "repo": up["repo"], "params_b": up["params_b"], "quant": up["quant"], "why": up["why"], "old": old, "role": old, - "fit": evaluate_fit(up["params_b"], up["quant"], 8192, ram_gb), + "fit": evaluate_fit(up["params_b"], up["quant"], 8192, ram_gb, name=up.get("name", "")), "optimal_ctx": max_ctx_for(up["params_b"], up["quant"], ram_gb), }) return out diff --git a/routers/models.py b/routers/models.py index 06e6293..c2428e6 100644 --- a/routers/models.py +++ b/routers/models.py @@ -58,7 +58,8 @@ class ChatReq(BaseModel): class UpdateReq(BaseModel): alias: str - ctx: int + ctx: int | None = None + ttl: int | None = None class DeleteReq(BaseModel): @@ -251,17 +252,20 @@ def update_model(req: UpdateReq): cfg = read_config() if req.alias not in cfg.get("models", {}): raise HTTPException(404, "Modell nicht gefunden") - + spec = cfg["models"][req.alias] - cmd = str(spec.get("cmd", "")) - - # Replace or add context size - if re.search(r'-(?:c|-ctx-size)\s+\d+', cmd): - cmd = re.sub(r'-(?:c|-ctx-size)\s+\d+', f'-c {req.ctx}', cmd) - else: - cmd = cmd.strip() + f" -c {req.ctx}\n" - - cfg["models"][req.alias]["cmd"] = LiteralScalarString(cmd) + + if req.ctx is not None: + cmd = str(spec.get("cmd", "")) + if re.search(r'-(?:c|-ctx-size)\s+\d+', cmd): + cmd = re.sub(r'-(?:c|-ctx-size)\s+\d+', f'-c {req.ctx}', cmd) + else: + cmd = cmd.strip() + f" -c {req.ctx}\n" + cfg["models"][req.alias]["cmd"] = LiteralScalarString(cmd) + + if req.ttl is not None: + cfg["models"][req.alias]["ttl"] = req.ttl + write_config(cfg) return {"ok": True} diff --git a/static/dist/main.js b/static/dist/main.js index b7fa1c3..6a07cea 100644 --- a/static/dist/main.js +++ b/static/dist/main.js @@ -1,51 +1,51 @@ -var tl=Object.defineProperty;var Mi=e=>{throw TypeError(e)};var nl=(e,t,a)=>t in e?tl(e,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[t]=a;var en=(e,t,a)=>nl(e,typeof t!="symbol"?t+"":t,a),Rr=(e,t,a)=>t.has(e)||Mi("Cannot "+a);var v=(e,t,a)=>(Rr(e,t,"read from private field"),a?a.call(e):t.get(e)),oe=(e,t,a)=>t.has(e)?Mi("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,a),ne=(e,t,a,r)=>(Rr(e,t,"write to private field"),r?r.call(e,a):t.set(e,a),a),ye=(e,t,a)=>(Rr(e,t,"access private method"),a);var oi=Array.isArray,al=Array.prototype.indexOf,mr=Array.prototype.includes,Tr=Array.from,rl=Object.defineProperty,Ga=Object.getOwnPropertyDescriptor,il=Object.getOwnPropertyDescriptors,sl=Object.prototype,ll=Array.prototype,es=Object.getPrototypeOf,Si=Object.isExtensible;const ol=()=>{};function dl(e){for(var t=0;t{e=r,t=i});return{promise:a,resolve:e,reject:t}}function Qa(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);const a=[];for(const r of e)if(a.push(r),a.length===t)break;return a}const zt=2,Ca=4,$r=8,ns=1<<24,mn=16,wn=32,Jn=64,jr=128,un=512,_t=1024,Lt=2048,Cn=4096,Ft=8192,vn=16384,Oa=32768,Jr=1<<25,La=65536,br=1<<17,cl=1<<18,Ra=1<<19,ul=1<<20,Tn=1<<25,pa=65536,wr=1<<21,ya=1<<22,Vn=1<<23,xa=Symbol("$state"),vl=Symbol(""),vr=Symbol("attributes"),Qr=Symbol("class"),Yr=Symbol("style"),qa=Symbol("text"),fr=Symbol("form reset"),Ar=new class extends Error{constructor(){super(...arguments);en(this,"name","StaleReactionError");en(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Qi;const fl=!!((Qi=globalThis.document)!=null&&Qi.contentType)&&globalThis.document.contentType.includes("xml");function as(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function pl(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function hl(e,t,a){throw new Error("https://svelte.dev/e/each_key_duplicate")}function gl(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function _l(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function ml(e){throw new Error("https://svelte.dev/e/effect_orphan")}function bl(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function wl(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function yl(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function xl(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function kl(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const Ml=1,Sl=2,rs=4,El=8,Tl=16,$l=1,Al=2,gt=Symbol("uninitialized"),is="http://www.w3.org/1999/xhtml",Cl="http://www.w3.org/2000/svg",Ll="http://www.w3.org/1998/Math/MathML";function Pl(){console.warn("https://svelte.dev/e/derived_inert")}function zl(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Dl(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function ss(e){return e===this.v}function Ol(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function ls(e){return!Ol(e,this.v)}let Pt=null;function Pa(e){Pt=e}function Yn(e,t=!1,a){Pt={p:Pt,i:!1,c:null,e:null,s:e,x:null,r:pe,l:null}}function Zn(e){var t=Pt,a=t.e;if(a!==null){t.e=null;for(var r of a)Cs(r)}return t.i=!0,Pt=t.p,{}}function os(){return!0}let ta=[];function ds(){var e=ta;ta=[],dl(e)}function ca(e){if(ta.length===0&&!Ha){var t=ta;queueMicrotask(()=>{t===ta&&ds()})}ta.push(e)}function Rl(){for(;ta.length>0;)ds()}function cs(e){var t=pe;if(t===null)return ve.f|=Vn,e;if((t.f&Oa)===0&&(t.f&Ca)===0)throw e;Un(e,t)}function Un(e,t){if(!(t!==null&&(t.f&vn)!==0)){for(;t!==null;){if((t.f&jr)!==0){if((t.f&Oa)===0)throw e;try{t.b.error(e);return}catch(a){e=a}}t=t.parent}throw e}}const Bl=-7169;function lt(e,t){e.f=e.f&Bl|t}function di(e){(e.f&un)!==0||e.deps===null?lt(e,_t):lt(e,Cn)}function us(e){if(e!==null)for(const t of e)(t.f&zt)===0||(t.f&pa)===0||(t.f^=pa,us(t.deps))}function vs(e,t,a){(e.f&Lt)!==0?t.add(e):(e.f&Cn)!==0&&a.add(e),us(e.deps),lt(e,_t)}function ql(e){let t=0,a=ga(0),r;return()=>{hi()&&(n(a),_i(()=>(t===0&&(r=lr(()=>e(()=>Va(a)))),t+=1,()=>{ca(()=>{t-=1,t===0&&(r==null||r(),r=void 0,Va(a))})})))}}var Nl=La|Ra;function Il(e,t,a,r){new Fl(e,t,a,r)}var ln,li,on,ia,Vt,dn,It,nn,zn,sa,Kn,ka,er,tr,Dn,Mr,at,Kl,Gl,Ul,Zr,pr,hr,Xr,ei;class Fl{constructor(t,a,r,i){oe(this,at);en(this,"parent");en(this,"is_pending",!1);en(this,"transform_error");oe(this,ln);oe(this,li,null);oe(this,on);oe(this,ia);oe(this,Vt);oe(this,dn,null);oe(this,It,null);oe(this,nn,null);oe(this,zn,null);oe(this,sa,0);oe(this,Kn,0);oe(this,ka,!1);oe(this,er,new Set);oe(this,tr,new Set);oe(this,Dn,null);oe(this,Mr,ql(()=>(ne(this,Dn,ga(v(this,sa))),()=>{ne(this,Dn,null)})));var o;ne(this,ln,t),ne(this,on,a),ne(this,ia,d=>{var c=pe;c.b=this,c.f|=jr,r(d)}),this.parent=pe.b,this.transform_error=i??((o=this.parent)==null?void 0:o.transform_error)??(d=>d),ne(this,Vt,mi(()=>{ye(this,at,Zr).call(this)},Nl))}defer_effect(t){vs(t,v(this,er),v(this,tr))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!v(this,on).pending}update_pending_count(t,a){ye(this,at,Xr).call(this,t,a),ne(this,sa,v(this,sa)+t),!(!v(this,Dn)||v(this,ka))&&(ne(this,ka,!0),ca(()=>{ne(this,ka,!1),v(this,Dn)&&za(v(this,Dn),v(this,sa))}))}get_effect_pending(){return v(this,Mr).call(this),n(v(this,Dn))}error(t){if(!v(this,on).onerror&&!v(this,on).failed)throw t;W!=null&&W.is_fork?(v(this,dn)&&W.skip_effect(v(this,dn)),v(this,It)&&W.skip_effect(v(this,It)),v(this,nn)&&W.skip_effect(v(this,nn)),W.oncommit(()=>{ye(this,at,ei).call(this,t)})):ye(this,at,ei).call(this,t)}}ln=new WeakMap,li=new WeakMap,on=new WeakMap,ia=new WeakMap,Vt=new WeakMap,dn=new WeakMap,It=new WeakMap,nn=new WeakMap,zn=new WeakMap,sa=new WeakMap,Kn=new WeakMap,ka=new WeakMap,er=new WeakMap,tr=new WeakMap,Dn=new WeakMap,Mr=new WeakMap,at=new WeakSet,Kl=function(){try{ne(this,dn,cn(()=>v(this,ia).call(this,v(this,ln))))}catch(t){this.error(t)}},Gl=function(t){const a=v(this,on).failed;a&&ne(this,nn,cn(()=>{a(v(this,ln),()=>t,()=>()=>{})}))},Ul=function(){const t=v(this,on).pending;t&&(this.is_pending=!0,ne(this,It,cn(()=>t(v(this,ln)))),ca(()=>{var a=ne(this,zn,document.createDocumentFragment()),r=Wn();a.append(r),ne(this,dn,ye(this,at,hr).call(this,()=>cn(()=>v(this,ia).call(this,r)))),v(this,Kn)===0&&(v(this,ln).before(a),ne(this,zn,null),va(v(this,It),()=>{ne(this,It,null)}),ye(this,at,pr).call(this,W))}))},Zr=function(){try{if(this.is_pending=this.has_pending_snippet(),ne(this,Kn,0),ne(this,sa,0),ne(this,dn,cn(()=>{v(this,ia).call(this,v(this,ln))})),v(this,Kn)>0){var t=ne(this,zn,document.createDocumentFragment());wi(v(this,dn),t);const a=v(this,on).pending;ne(this,It,cn(()=>a(v(this,ln))))}else ye(this,at,pr).call(this,W)}catch(a){this.error(a)}},pr=function(t){this.is_pending=!1,t.transfer_effects(v(this,er),v(this,tr))},hr=function(t){var a=pe,r=ve,i=Pt;Ln(v(this,Vt)),fn(v(this,Vt)),Pa(v(this,Vt).ctx);try{return ha.ensure(),t()}catch(o){return cs(o),null}finally{Ln(a),fn(r),Pa(i)}},Xr=function(t,a){var r;if(!this.has_pending_snippet()){this.parent&&ye(r=this.parent,at,Xr).call(r,t,a);return}ne(this,Kn,v(this,Kn)+t),v(this,Kn)===0&&(ye(this,at,pr).call(this,a),v(this,It)&&va(v(this,It),()=>{ne(this,It,null)}),v(this,zn)&&(v(this,ln).before(v(this,zn)),ne(this,zn,null)))},ei=function(t){v(this,dn)&&(Jt(v(this,dn)),ne(this,dn,null)),v(this,It)&&(Jt(v(this,It)),ne(this,It,null)),v(this,nn)&&(Jt(v(this,nn)),ne(this,nn,null));var a=v(this,on).onerror;let r=v(this,on).failed;var i=!1,o=!1;const d=()=>{if(i){Dl();return}i=!0,o&&kl(),v(this,nn)!==null&&va(v(this,nn),()=>{ne(this,nn,null)}),ye(this,at,hr).call(this,()=>{ye(this,at,Zr).call(this)})},c=u=>{try{o=!0,a==null||a(u,d),o=!1}catch(f){Un(f,v(this,Vt)&&v(this,Vt).parent)}r&&ne(this,nn,ye(this,at,hr).call(this,()=>{try{return cn(()=>{var f=pe;f.b=this,f.f|=jr,r(v(this,ln),()=>u,()=>d)})}catch(f){return Un(f,v(this,Vt).parent),null}}))};ca(()=>{var u;try{u=this.transform_error(t)}catch(f){Un(f,v(this,Vt)&&v(this,Vt).parent);return}u!==null&&typeof u=="object"&&typeof u.then=="function"?u.then(c,f=>Un(f,v(this,Vt)&&v(this,Vt).parent)):c(u)})};function Hl(e,t,a,r){const i=ci;var o=e.filter(b=>!b.settled),d=t.map(i);if(a.length===0&&o.length===0){r(d);return}var c=pe,u=Vl(),f=o.length===1?o[0].promise:o.length>1?Promise.all(o.map(b=>b.promise)):null;function g(b){if((c.f&vn)===0){u();try{r([...d,...b])}catch(S){Un(S,c)}yr()}}var $=fs();if(a.length===0){f.then(()=>g([])).finally($);return}function h(){Promise.all(a.map(b=>Wl(b))).then(g).catch(b=>Un(b,c)).finally($)}f?f.then(()=>{u(),h(),yr()}):h()}function Vl(){var e=pe,t=ve,a=Pt,r=W;return function(o=!0){Ln(e),fn(t),Pa(a),o&&(e.f&vn)===0&&(r==null||r.activate(),r==null||r.apply())}}function yr(e=!0){Ln(null),fn(null),Pa(null),e&&(W==null||W.deactivate())}function fs(){var e=pe,t=e.b,a=W,r=!!(t!=null&&t.is_rendered());return t==null||t.update_pending_count(1,a),a.increment(r,e),()=>{t==null||t.update_pending_count(-1,a),a.decrement(r,e)}}function ci(e){var t=zt|Lt;return pe!==null&&(pe.f|=Ra),{ctx:Pt,deps:null,effects:null,equals:ss,f:t,fn:e,reactions:null,rv:0,v:gt,wv:0,parent:pe,ac:null}}const Na=Symbol("obsolete");function Wl(e,t,a){let r=pe;r===null&&pl();var i=void 0,o=ga(gt),d=!ve,c=new Set;return uo(()=>{var b,S;var u=pe,f=ts();i=f.promise;try{Promise.resolve(e()).then(f.resolve,P=>{P!==Ar&&f.reject(P)}).finally(yr)}catch(P){f.reject(P),yr()}var g=W;if(d){if((u.f&Oa)!==0)var $=fs();if((b=r.b)!=null&&b.is_rendered())(S=g.async_deriveds.get(u))==null||S.reject(Na);else for(const P of c.values())P.reject(Na);c.add(f),g.async_deriveds.set(u,f)}const h=(P,E=void 0)=>{$==null||$(),c.delete(f),E!==Na&&(g.activate(),E?(o.f|=Vn,za(o,E)):((o.f&Vn)!==0&&(o.f^=Vn),za(o,P)),g.deactivate())};f.promise.then(h,P=>h(null,P||"unknown"))}),As(()=>{for(const u of c)u.reject(Na)}),new Promise(u=>{function f(g){function $(){g===i?u(o):f(i)}g.then($,$)}f(i)})}function N(e){const t=ci(e);return Bs(t),t}function jl(e){const t=ci(e);return t.equals=ls,t}function Jl(e){var t=e.effects;if(t!==null){e.effects=null;for(var a=0;athis.schedule(r)){var r=v(this,Sn).get(t);if(r){v(this,Sn).delete(t);for(var i of r.d)lt(i,Lt),a(i);for(i of r.m)lt(i,Cn),a(i)}v(this,Aa).add(t)}capture(t,a,r=!1){t.v!==gt&&!this.previous.has(t)&&this.previous.set(t,t.v),(t.f&Vn)===0&&(this.current.set(t,[a,r]),Ct==null||Ct.set(t,a)),this.is_fork||(t.v=a)}activate(){W=this}deactivate(){W=null,Ct=null}flush(){try{qr=!0,W=this,ye(this,Te,Ia).call(this)}finally{Ei=0,ti=null,wa=null,gr=null,qr=!1,W=null,Ct=null,ua.clear()}}discard(){var t;for(const a of v(this,Ea))a(this);v(this,Ea).clear();for(const a of this.async_deriveds.values())a.reject(Na);ye(this,Te,Fa).call(this),(t=v(this,$a))==null||t.resolve()}register_created_effect(t){v(this,nr).push(t)}increment(t,a){if(ne(this,Ta,v(this,Ta)+1),t){let r=v(this,On).get(a)??0;v(this,On).set(a,r+1)}}decrement(t,a){if(ne(this,Ta,v(this,Ta)-1),t){let r=v(this,On).get(a)??0;r===1?v(this,On).delete(a):v(this,On).set(a,r-1)}v(this,oa)||(ne(this,oa,!0),ca(()=>{ne(this,oa,!1),this.linked&&this.flush()}))}transfer_effects(t,a){for(const r of t)v(this,Rn).add(r);for(const r of a)v(this,hn).add(r);t.clear(),a.clear()}oncommit(t){v(this,Sa).add(t)}ondiscard(t){v(this,Ea).add(t)}settled(){return(v(this,$a)??ne(this,$a,ts())).promise}static ensure(){if(W===null){const t=W=new Sr;!qr&&!Ha&&ca(()=>{v(t,Ma)||t.flush()})}return W}apply(){{Ct=null;return}}schedule(t){var i;if(ti=t,(i=t.b)!=null&&i.is_pending&&(t.f&(Ca|$r|ns))!==0&&(t.f&Oa)===0){t.b.defer_effect(t);return}for(var a=t;a.parent!==null;){a=a.parent;var r=a.f;if(wa!==null&&a===pe&&(ve===null||(ve.f&zt)===0))return;if((r&(Jn|wn))!==0){if((r&_t)===0)return;a.f^=_t}}v(this,Wt).push(a)}};Ma=new WeakMap,Gn=new WeakMap,la=new WeakMap,Sa=new WeakMap,Ea=new WeakMap,Ta=new WeakMap,On=new WeakMap,$a=new WeakMap,Wt=new WeakMap,nr=new WeakMap,Rn=new WeakMap,hn=new WeakMap,Sn=new WeakMap,Aa=new WeakMap,oa=new WeakMap,Te=new WeakSet,ni=function(){if(this.is_fork)return!0;for(const r of v(this,On).keys()){for(var t=r,a=!1;t.parent!==null;){if(v(this,Sn).has(t)){a=!0;break}t=t.parent}if(!a)return!0}return!1},Ia=function(){var u,f,g,$;ne(this,Ma,!0),Ei++>1e3&&(ye(this,Te,Fa).call(this),eo());for(const h of v(this,Rn))v(this,hn).delete(h),lt(h,Lt),this.schedule(h);for(const h of v(this,hn))lt(h,Cn),this.schedule(h);const t=v(this,Wt);ne(this,Wt,[]),this.apply();var a=wa=[],r=[],i=gr=[];for(const h of t)try{ye(this,Te,ai).call(this,h,a,r)}catch(b){throw ws(h),ye(this,Te,ni).call(this)||this.discard(),b}if(W=null,i.length>0){var o=Sr.ensure();for(const h of i)o.schedule(h)}if(wa=null,gr=null,ye(this,Te,ni).call(this)){ye(this,Te,ba).call(this,r),ye(this,Te,ba).call(this,a);for(const[h,b]of v(this,Sn))bs(h,b);i.length>0&&ye(u=W,Te,Ia).call(u);return}const d=ye(this,Te,gs).call(this);if(d){ye(this,Te,ba).call(this,r),ye(this,Te,ba).call(this,a),ye(f=d,Te,_s).call(f,this);return}v(this,Rn).clear(),v(this,hn).clear();for(const h of v(this,Sa))h(this);v(this,Sa).clear(),Ua=this,Ti(r),Ti(a),Ua=null,(g=v(this,$a))==null||g.resolve();var c=W;if(v(this,Ta)===0&&(v(this,Wt).length===0||c!==null)&&ye(this,Te,Fa).call(this),v(this,Wt).length>0)if(c!==null){const h=c;v(h,Wt).push(...v(this,Wt).filter(b=>!v(h,Wt).includes(b)))}else c=this;c!==null&&ye($=c,Te,Ia).call($)},ai=function(t,a,r){t.f^=_t;for(var i=t.first;i!==null;){var o=i.f,d=(o&(wn|Jn))!==0,c=d&&(o&_t)!==0,u=c||(o&Ft)!==0||v(this,Sn).has(i);if(!u&&i.fn!==null){d?i.f^=_t:(o&Ca)!==0?a.push(i):sr(i)&&((o&mn)!==0&&v(this,hn).add(i),Da(i));var f=i.first;if(f!==null){i=f;continue}}for(;i!==null;){var g=i.next;if(g!==null){i=g;break}i=i.parent}}},gs=function(){for(var t=v(this,Gn);t!==null;){if(!t.is_fork){for(const[a,[,r]]of this.current)if(t.current.has(a)&&!r)return t}t=v(t,Gn)}return null},_s=function(t){var r;for(const[i,o]of t.current)!this.previous.has(i)&&t.previous.has(i)&&this.previous.set(i,t.previous.get(i)),this.current.set(i,o);for(const[i,o]of t.async_deriveds){const d=this.async_deriveds.get(i);d&&o.promise.then(d.resolve).catch(d.reject)}t.async_deriveds.clear(),this.transfer_effects(v(t,Rn),v(t,hn));const a=i=>{var o=i.reactions;if(o!==null)for(const u of o){var d=u.f;if((d&zt)!==0)a(u);else{var c=u;d&(ya|mn)&&!this.async_deriveds.has(c)&&(v(this,hn).delete(c),lt(c,Lt),this.schedule(c))}}};for(const i of this.current.keys())a(i);this.oncommit(()=>t.discard()),ye(r=t,Te,Fa).call(r),W=this,ye(this,Te,Ia).call(this)},ba=function(t){for(var a=0;a!h.current.get(b)[1]);if(!(!v(h,Ma)||i.length===0)){var o=i.filter(b=>!this.current.has(b));if(o.length===0)t&&h.discard();else if(a.length>0){if(t)for(const b of v(this,Aa))h.unskip_effect(b,S=>{var P;(S.f&(mn|ya))!==0?h.schedule(S):ye(P=h,Te,ba).call(P,[S])});h.activate();var d=new Set,c=new Map;for(var u of a)ms(u,o,d,c);c=new Map;var f=[...h.current].filter(([b,S])=>{const P=this.current.get(b);return P?P[0]!==S[0]||P[1]!==S[1]:!0}).map(([b])=>b);if(f.length>0)for(const b of v(this,nr))(b.f&(vn|Ft|br))===0&&vi(b,f,c)&&((b.f&(ya|mn))!==0?(lt(b,Lt),h.schedule(b)):v(h,Rn).add(b));if(v(h,Wt).length>0&&!v(h,oa)){h.apply();for(var g of v(h,Wt))ye($=h,Te,ai).call($,g,[],[]);ne(h,Wt,[])}h.deactivate()}}}},Fa=function(){if(this.linked){var t=v(this,Gn),a=v(this,la);t===null?Br=a:ne(t,la,a),a===null?ma=t:ne(a,Gn,t),this.linked=!1}};let ha=Sr;function Xl(e){var t=Ha;Ha=!0;try{for(var a;;){if(Rl(),W===null)return a;W.flush()}}finally{Ha=t}}function eo(){try{bl()}catch(e){Un(e,ti)}}let pn=null;function Ti(e){var t=e.length;if(t!==0){for(var a=0;a0)){ua.clear();for(const i of pn){if((i.f&(vn|Ft))!==0)continue;const o=[i];let d=i.parent;for(;d!==null;)pn.has(d)&&(pn.delete(d),o.push(d)),d=d.parent;for(let c=o.length-1;c>=0;c--){const u=o[c];(u.f&(vn|Ft))===0&&Da(u)}}pn.clear()}}pn=null}}function ms(e,t,a,r){if(!a.has(e)&&(a.add(e),e.reactions!==null))for(const i of e.reactions){const o=i.f;(o&zt)!==0?ms(i,t,a,r):(o&(ya|mn))!==0&&(o&Lt)===0&&vi(i,t,r)&&(lt(i,Lt),fi(i))}}function vi(e,t,a){const r=a.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const i of e.deps){if(mr.call(t,i))return!0;if((i.f&zt)!==0&&vi(i,t,a))return a.set(i,!0),!0}return a.set(e,!1),!1}function fi(e){W.schedule(e)}function bs(e,t){if(!((e.f&wn)!==0&&(e.f&_t)!==0)){(e.f&Lt)!==0?t.d.push(e):(e.f&Cn)!==0&&t.m.push(e),lt(e,_t);for(var a=e.first;a!==null;)bs(a,t),a=a.next}}function ws(e){lt(e,_t);for(var t=e.first;t!==null;)ws(t),t=t.next}let xr=new Set;const ua=new Map;let ys=!1;function ga(e,t){var a={f:0,v:e,reactions:null,equals:ss,rv:0,wv:0};return a}function R(e,t){const a=ga(e);return Bs(a),a}function to(e,t=!1,a=!0){const r=ga(e);return t||(r.equals=ls),r}function _(e,t,a=!1){ve!==null&&(!bn||(ve.f&br)!==0)&&os()&&(ve.f&(zt|mn|ya|br))!==0&&($n===null||!$n.has(e))&&xl();let r=a?Bt(t):t;return za(e,r,gr)}function za(e,t,a=null){if(!e.equals(t)){ua.set(e,Qn?t:e.v);var r=ha.ensure();if(r.capture(e,t),(e.f&zt)!==0){const i=e;(e.f&Lt)!==0&&ui(i),Ct===null&&di(i)}e.wv=Ns(),xs(e,Lt,a),pe!==null&&(pe.f&_t)!==0&&(pe.f&(wn|Jn))===0&&(sn===null?fo([e]):sn.push(e)),!r.is_fork&&xr.size>0&&!ys&&no()}return t}function no(){ys=!1;for(const e of xr){(e.f&_t)!==0&<(e,Cn);let t;try{t=sr(e)}catch{t=!0}t&&Da(e)}xr.clear()}function Va(e){_(e,e.v+1)}function xs(e,t,a){var r=e.reactions;if(r!==null)for(var i=r.length,o=0;o{if(fa===o)return c();var u=ve,f=fa;fn(null),Pi(o);var g=c();return fn(u),Pi(f),g};return r&&a.set("length",R(e.length)),new Proxy(e,{defineProperty(c,u,f){(!("value"in f)||f.configurable===!1||f.enumerable===!1||f.writable===!1)&&wl();var g=a.get(u);return g===void 0?d(()=>{var $=R(f.value);return a.set(u,$),$}):_(g,f.value,!0),!0},deleteProperty(c,u){var f=a.get(u);if(f===void 0){if(u in c){const g=d(()=>R(gt));a.set(u,g),Va(i)}}else _(f,gt),Va(i);return!0},get(c,u,f){var b;if(u===xa)return e;var g=a.get(u),$=u in c;if(g===void 0&&(!$||(b=Ga(c,u))!=null&&b.writable)&&(g=d(()=>{var S=Bt($?c[u]:gt),P=R(S);return P}),a.set(u,g)),g!==void 0){var h=n(g);return h===gt?void 0:h}return Reflect.get(c,u,f)},getOwnPropertyDescriptor(c,u){var f=Reflect.getOwnPropertyDescriptor(c,u);if(f&&"value"in f){var g=a.get(u);g&&(f.value=n(g))}else if(f===void 0){var $=a.get(u),h=$==null?void 0:$.v;if($!==void 0&&h!==gt)return{enumerable:!0,configurable:!0,value:h,writable:!0}}return f},has(c,u){var h;if(u===xa)return!0;var f=a.get(u),g=f!==void 0&&f.v!==gt||Reflect.has(c,u);if(f!==void 0||pe!==null&&(!g||(h=Ga(c,u))!=null&&h.writable)){f===void 0&&(f=d(()=>{var b=g?Bt(c[u]):gt,S=R(b);return S}),a.set(u,f));var $=n(f);if($===gt)return!1}return g},set(c,u,f,g){var Y;var $=a.get(u),h=u in c;if(r&&u==="length")for(var b=f;b<$.v;b+=1){var S=a.get(b+"");S!==void 0?_(S,gt):b in c&&(S=d(()=>R(gt)),a.set(b+"",S))}if($===void 0)(!h||(Y=Ga(c,u))!=null&&Y.writable)&&($=d(()=>R(void 0)),_($,Bt(f)),a.set(u,$));else{h=$.v!==gt;var P=d(()=>Bt(f));_($,P)}var E=Reflect.getOwnPropertyDescriptor(c,u);if(E!=null&&E.set&&E.set.call(g,f),!h){if(r&&typeof u=="string"){var I=a.get("length"),de=Number(u);Number.isInteger(de)&&de>=I.v&&_(I,de+1)}Va(i)}return!0},ownKeys(c){n(i);var u=Reflect.ownKeys(c).filter($=>{var h=a.get($);return h===void 0||h.v!==gt});for(var[f,g]of a)g.v!==gt&&!(f in c)&&u.push(f);return u},setPrototypeOf(){yl()}})}function $i(e){try{if(e!==null&&typeof e=="object"&&xa in e)return e[xa]}catch{}return e}function ao(e,t){return Object.is($i(e),$i(t))}var Ai,ks,Ms,Ss;function ro(){if(Ai===void 0){Ai=window,ks=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,a=Text.prototype;Ms=Ga(t,"firstChild").get,Ss=Ga(t,"nextSibling").get,Si(e)&&(e[Qr]=void 0,e[vr]=null,e[Yr]=void 0,e.__e=void 0),Si(a)&&(a[qa]=void 0)}}function Wn(e=""){return document.createTextNode(e)}function Hn(e){return Ms.call(e)}function ir(e){return Ss.call(e)}function s(e,t){return Hn(e)}function ft(e,t=!1){{var a=Hn(e);return a instanceof Comment&&a.data===""?ir(a):a}}function l(e,t=1,a=!1){let r=e;for(;t--;)r=ir(r);return r}function io(e){e.textContent=""}function Es(){return!1}function Ts(e,t,a){return t==null||t===is?a?document.createElement(e,{is:a}):document.createElement(e):a?document.createElementNS(t,e,{is:a}):document.createElementNS(t,e)}let Ci=!1;function so(){Ci||(Ci=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const a of e.target.elements)(t=a[fr])==null||t.call(a)})},{capture:!0}))}function pi(e){var t=ve,a=pe;fn(null),Ln(null);try{return e()}finally{fn(t),Ln(a)}}function $s(e,t,a,r=a){e.addEventListener(t,()=>pi(a));const i=e[fr];i?e[fr]=()=>{i(),r(!0)}:e[fr]=()=>r(!0),so()}function lo(e){pe===null&&(ve===null&&ml(),_l()),Qn&&gl()}function oo(e,t){var a=t.last;a===null?t.last=t.first=e:(a.next=e,e.prev=a,t.last=e)}function Bn(e,t){var a=pe;a!==null&&(a.f&Ft)!==0&&(e|=Ft);var r={ctx:Pt,deps:null,nodes:null,f:e|Lt|un,first:null,fn:t,last:null,next:null,parent:a,b:a&&a.b,prev:null,teardown:null,wv:0,ac:null};W==null||W.register_created_effect(r);var i=r;if((e&Ca)!==0)wa!==null?wa.push(r):ha.ensure().schedule(r);else if(t!==null){try{Da(r)}catch(d){throw Jt(r),d}i.deps===null&&i.teardown===null&&i.nodes===null&&i.first===i.last&&(i.f&Ra)===0&&(i=i.first,(e&mn)!==0&&(e&La)!==0&&i!==null&&(i.f|=La))}if(i!==null&&(i.parent=a,a!==null&&oo(i,a),ve!==null&&(ve.f&zt)!==0&&(e&Jn)===0)){var o=ve;(o.effects??(o.effects=[])).push(i)}return r}function hi(){return ve!==null&&!bn}function As(e){const t=Bn($r,null);return lt(t,_t),t.teardown=e,t}function gi(e){lo();var t=pe.f,a=!ve&&(t&wn)!==0&&Pt!==null&&!Pt.i;if(a){var r=Pt;(r.e??(r.e=[])).push(e)}else return Cs(e)}function Cs(e){return Bn(Ca|ul,e)}function co(e){ha.ensure();const t=Bn(Jn|Ra,e);return(a={})=>new Promise(r=>{a.outro?va(t,()=>{Jt(t),r(void 0)}):(Jt(t),r(void 0))})}function Ls(e){return Bn(Ca,e)}function uo(e){return Bn(ya|Ra,e)}function _i(e,t=0){return Bn($r|t,e)}function z(e,t=[],a=[],r=[]){Hl(r,t,a,i=>{Bn($r,()=>{e(...i.map(n))})})}function mi(e,t=0){var a=Bn(mn|t,e);return a}function cn(e){return Bn(wn|Ra,e)}function Ps(e){var t=e.teardown;if(t!==null){const a=Qn,r=ve;Li(!0),fn(null);try{t.call(null)}finally{Li(a),fn(r)}}}function bi(e,t=!1){var a=e.first;for(e.first=e.last=null;a!==null;){const i=a.ac;i!==null&&pi(()=>{i.abort(Ar)});var r=a.next;(a.f&Jn)!==0?a.parent=null:Jt(a,t),a=r}}function vo(e){for(var t=e.first;t!==null;){var a=t.next;(t.f&wn)===0&&Jt(t),t=a}}function Jt(e,t=!0){var a=!1;(t||(e.f&cl)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(zs(e.nodes.start,e.nodes.end),a=!0),e.f|=Jr,bi(e,t&&!a),Ya(e,0);var r=e.nodes&&e.nodes.t;if(r!==null)for(const o of r)o.stop();Ps(e),e.f^=Jr,e.f|=vn;var i=e.parent;i!==null&&i.first!==null&&Ds(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=e.b=null}function zs(e,t){for(;e!==null;){var a=e===t?null:ir(e);e.remove(),e=a}}function Ds(e){var t=e.parent,a=e.prev,r=e.next;a!==null&&(a.next=r),r!==null&&(r.prev=a),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=a))}function va(e,t,a=!0){var r=[];Os(e,r,!0);var i=()=>{a&&Jt(e),t&&t()},o=r.length;if(o>0){var d=()=>--o||i();for(var c of r)c.out(d)}else i()}function Os(e,t,a){if((e.f&Ft)===0){e.f^=Ft;var r=e.nodes&&e.nodes.t;if(r!==null)for(const c of r)(c.is_global||a)&&t.push(c);for(var i=e.first;i!==null;){var o=i.next;if((i.f&Jn)===0){var d=(i.f&La)!==0||(i.f&wn)!==0&&(e.f&mn)!==0;Os(i,t,d?a:!1)}i=o}}}function kr(e){Rs(e,!0)}function Rs(e,t){if((e.f&Ft)!==0){e.f^=Ft,(e.f&_t)===0&&(lt(e,Lt),ha.ensure().schedule(e));for(var a=e.first;a!==null;){var r=a.next,i=(a.f&La)!==0||(a.f&wn)!==0;Rs(a,i?t:!1),a=r}var o=e.nodes&&e.nodes.t;if(o!==null)for(const d of o)(d.is_global||t)&&d.in()}}function wi(e,t){if(e.nodes)for(var a=e.nodes.start,r=e.nodes.end;a!==null;){var i=a===r?null:ir(a);t.append(a),a=i}}let _r=!1,Qn=!1;function Li(e){Qn=e}let ve=null,bn=!1;function fn(e){ve=e}let pe=null;function Ln(e){pe=e}let $n=null;function Bs(e){ve!==null&&($n??($n=new Set)).add(e)}let jt=null,tn=0,sn=null;function fo(e){sn=e}let qs=1,na=0,fa=na;function Pi(e){fa=e}function Ns(){return++qs}function sr(e){var t=e.f;if((t&Lt)!==0)return!0;if(t&zt&&(e.f&=~pa),(t&Cn)!==0){for(var a=e.deps,r=a.length,i=0;ie.wv)return!0}(t&un)!==0&&Ct===null&<(e,_t)}return!1}function Is(e,t,a=!0){var r=e.reactions;if(r!==null&&!($n!==null&&$n.has(e)))for(var i=0;i{e.ac.abort(Ar)}),e.ac=null);try{e.f|=wr;var g=e.fn,$=g();e.f|=Oa;var h=e.deps,b=W==null?void 0:W.is_fork;if(jt!==null){var S;if(b||Ya(e,tn),h!==null&&tn>0)for(h.length=tn+jt.length,S=0;S{throw I});throw h}}finally{e[aa]=t,delete e.currentTarget,fn(g),Ln($)}}}var Yi;const Nr=((Yi=globalThis==null?void 0:globalThis.window)==null?void 0:Yi.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function mo(e){return(Nr==null?void 0:Nr.createHTML(e))??e}function bo(e){var t=Ts("template");return t.innerHTML=mo(e.replaceAll("","")),t.content}function Za(e,t){var a=pe;a.nodes===null&&(a.nodes={start:e,end:t,a:null,t:null})}function M(e,t){var a=(t&$l)!==0,r=(t&Al)!==0,i,o=!e.startsWith("");return()=>{i===void 0&&(i=bo(o?e:""+e),a||(i=Hn(i)));var d=r||ks?document.importNode(i,!0):i.cloneNode(!0);if(a){var c=Hn(d),u=d.lastChild;Za(c,u)}else Za(d,d);return d}}function Cr(){var e=document.createDocumentFragment(),t=document.createComment(""),a=Wn();return e.append(t,a),Za(t,a),e}function x(e,t){e!==null&&e.before(t)}function m(e,t){var a=t==null?"":typeof t=="object"?`${t}`:t;a!==(e[qa]??(e[qa]=e.nodeValue))&&(e[qa]=a,e.nodeValue=`${a}`)}function wo(e,t){return yo(e,t)}const cr=new Map;function yo(e,{target:t,anchor:a,props:r={},events:i,context:o,intro:d=!0,transformError:c}){ro();var u=void 0,f=co(()=>{var g=a??t.appendChild(Wn());Il(g,{pending:()=>{}},b=>{Yn({});var S=Pt;o&&(S.c=o),i&&(r.$$events=i),u=e(b,r)||{},Zn()},c);var $=new Set,h=b=>{for(var S=0;S{var E;for(var b of $)for(const I of[t,document]){var S=cr.get(I),P=S.get(b);--P==0?(I.removeEventListener(b,Di),S.delete(b),S.size===0&&cr.delete(I)):S.set(b,P)}ri.delete(h),g!==a&&((E=g.parentNode)==null||E.removeChild(g))}});return xo.set(u,f),u}let xo=new WeakMap;var gn,En,an,da,ar,rr,Er;class ko{constructor(t,a=!0){en(this,"anchor");oe(this,gn,new Map);oe(this,En,new Map);oe(this,an,new Map);oe(this,da,new Set);oe(this,ar,!0);oe(this,rr,t=>{if(v(this,gn).has(t)){var a=v(this,gn).get(t),r=v(this,En).get(a);if(r)kr(r),v(this,da).delete(a);else{var i=v(this,an).get(a);i&&(kr(i.effect),v(this,En).set(a,i.effect),v(this,an).delete(a),i.fragment.lastChild.remove(),this.anchor.before(i.fragment),r=i.effect)}for(const[o,d]of v(this,gn)){if(v(this,gn).delete(o),o===t)break;const c=v(this,an).get(d);c&&(Jt(c.effect),v(this,an).delete(d))}for(const[o,d]of v(this,En)){if(o===a||v(this,da).has(o))continue;const c=()=>{if(Array.from(v(this,gn).values()).includes(o)){var f=document.createDocumentFragment();wi(d,f),f.append(Wn()),v(this,an).set(o,{effect:d,fragment:f})}else Jt(d);v(this,da).delete(o),v(this,En).delete(o)};v(this,ar)||!r?(v(this,da).add(o),va(d,c,!1)):c()}}});oe(this,Er,t=>{v(this,gn).delete(t);const a=Array.from(v(this,gn).values());for(const[r,i]of v(this,an))a.includes(r)||(Jt(i.effect),v(this,an).delete(r))});this.anchor=t,ne(this,ar,a)}ensure(t,a){var r=W,i=Es();if(a&&!v(this,En).has(t)&&!v(this,an).has(t))if(i){var o=document.createDocumentFragment(),d=Wn();o.append(d),v(this,an).set(t,{effect:cn(()=>a(d)),fragment:o})}else v(this,En).set(t,cn(()=>a(this.anchor)));if(v(this,gn).set(r,t),i){for(const[c,u]of v(this,En))c===t?r.unskip_effect(u):r.skip_effect(u);for(const[c,u]of v(this,an))c===t?r.unskip_effect(u.effect):r.skip_effect(u.effect);r.oncommit(v(this,rr)),r.ondiscard(v(this,Er))}else v(this,rr).call(this,r)}}gn=new WeakMap,En=new WeakMap,an=new WeakMap,da=new WeakMap,ar=new WeakMap,rr=new WeakMap,Er=new WeakMap;function V(e,t,a=!1){var r=new ko(e),i=a?La:0;function o(d,c){r.ensure(d,c)}mi(()=>{var d=!1;t((c,u=0)=>{d=!0,o(u,c)}),d||o(-1,null)},i)}function Ge(e,t){return t}function Mo(e,t,a){for(var r=[],i=t.length,o,d=t.length,c=0;c{if(o){if(o.pending.delete($),o.done.add($),o.pending.size===0){var h=e.outrogroups;ii(e,Tr(o.done)),h.delete(o),h.size===0&&(e.outrogroups=null)}}else d-=1},!1)}if(d===0){var u=r.length===0&&a!==null;if(u){var f=a,g=f.parentNode;io(g),g.append(f),e.items.clear()}ii(e,t,!u)}else o={pending:new Set(t),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(o)}function ii(e,t,a=!0){var r;if(e.pending.size>0){r=new Set;for(const d of e.pending.values())for(const c of d)r.add(e.items.get(c).e)}for(var i=0;i{var Y=a();return oi(Y)?Y:Y==null?[]:Tr(Y)}),h,b=new Map,S=!0;function P(Y){(de.effect.f&vn)===0&&(de.pending.delete(Y),de.fallback=g,So(de,h,d,t,r),g!==null&&(h.length===0?(g.f&Tn)===0?kr(g):(g.f^=Tn,Ka(g,null,d)):va(g,()=>{g=null})))}function E(Y){de.pending.delete(Y)}var I=mi(()=>{h=n($);for(var Y=h.length,ce=new Set,me=W,U=Es(),F=0;Fo(d)):(g=cn(()=>o(Oi??(Oi=Wn()))),g.f|=Tn)),Y>ce.size&&hl(),!S)if(b.set(me,ce),U){for(const[Me,He]of c)ce.has(Me)||me.skip_effect(He.e);me.oncommit(P),me.ondiscard(E)}else P(me);n($)}),de={effect:I,items:c,pending:b,outrogroups:null,fallback:g};S=!1}function Ba(e){for(;e!==null&&(e.f&wn)===0;)e=e.next;return e}function So(e,t,a,r,i){var _e,Me,He,T,J,se,$e,Q,be;var o=(r&El)!==0,d=t.length,c=e.items,u=Ba(e.effect.first),f,g=null,$,h=[],b=[],S,P,E,I;if(o)for(I=0;I0){var he=(r&rs)!==0&&d===0?a:null;if(o){for(I=0;I{var X,ze;if($!==void 0)for(E of $)(ze=(X=E.nodes)==null?void 0:X.a)==null||ze.apply()})}function Eo(e,t,a,r,i,o,d,c){var u=(d&Ml)!==0?(d&Tl)===0?to(a,!1,!1):ga(a):null,f=(d&Sl)!==0?ga(i):null;return{v:u,i:f,e:cn(()=>(o(t,u??a,f??i,c),()=>{e.delete(r)}))}}function Ka(e,t,a){if(e.nodes)for(var r=e.nodes.start,i=e.nodes.end,o=t&&(t.f&Tn)===0?t.nodes.start:a;r!==null;){var d=ir(r);if(o.before(r),r===i)return;r=d}}function In(e,t,a){t===null?e.effect.first=a:t.next=a,a===null?e.effect.last=t:a.prev=t}function Qe(e,t,a=!1,r=!1,i=!1,o=!1){var d=e,c="";if(a)var u=e;z(()=>{var f=pe;if(c!==(c=t()??"")){if(a){f.nodes=null,u.innerHTML=c,c!==""&&Za(Hn(u),u.lastChild);return}if(f.nodes!==null&&(zs(f.nodes.start,f.nodes.end),f.nodes=null),c!==""){var g=r?Cl:i?Ll:void 0,$=Ts(r?"svg":i?"math":"template",g);$.innerHTML=c;var h=r||i?$:$.content;if(Za(Hn(h),h.lastChild),r||i)for(;Hn(h);)d.before(Hn(h));else d.before(h)}}})}function To(e,t,a){var r=e==null?"":""+e;return r===""?null:r}function $o(e,t){return e==null?null:String(e)}function st(e,t,a,r,i,o){var d=e[Qr];if(d!==a||d===void 0){var c=To(a);c==null?e.removeAttribute("class"):e.className=c,e[Qr]=a}return o}function jn(e,t,a,r){var i=e[Yr];if(i!==t){var o=$o(t);o==null?e.removeAttribute("style"):e.style.cssText=o,e[Yr]=t}return r}function Hs(e,t,a=!1){if(e.multiple){if(t==null)return;if(!oi(t))return zl();for(var r of e.options)r.selected=t.includes(ja(r));return}for(r of e.options){var i=ja(r);if(ao(i,t)){r.selected=!0;return}}(!a||t!==void 0)&&(e.selectedIndex=-1)}function Ao(e){var t=new MutationObserver(()=>{Hs(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),As(()=>{t.disconnect()})}function Wa(e,t,a=t){var r=new WeakSet,i=!0;$s(e,"change",o=>{var d=o?"[selected]":":checked",c;if(e.multiple)c=[].map.call(e.querySelectorAll(d),ja);else{var u=e.querySelector(d)??e.querySelector("option:not([disabled])");c=u&&ja(u)}a(c),e.__value=c,W!==null&&r.add(W)}),Ls(()=>{var o=t();if(e===document.activeElement){var d=W;if(r.has(d))return}if(Hs(e,o,i),i&&o===void 0){var c=e.querySelector(":checked");c!==null&&(o=ja(c),a(o))}e.__value=o,i=!1}),Ao(e)}function ja(e){return"__value"in e?e.__value:e.value}const Co=Symbol("is custom element"),Lo=Symbol("is html"),Po=fl?"progress":"PROGRESS";function ur(e,t){var a=Vs(e);a.value===(a.value=t??void 0)||e.value===t&&(t!==0||e.nodeName!==Po)||(e.value=t??"")}function Lr(e,t,a,r){var i=Vs(e);i[t]!==(i[t]=a)&&(t==="loading"&&(e[vl]=a),a==null?e.removeAttribute(t):typeof a!="string"&&zo(e).includes(t)?e[t]=a:e.setAttribute(t,a))}function Vs(e){return e[vr]??(e[vr]={[Co]:e.nodeName.includes("-"),[Lo]:e.namespaceURI===is})}var Ri=new Map;function zo(e){var t=e.getAttribute("is")||e.nodeName,a=Ri.get(t);if(a)return a;Ri.set(t,a=[]);for(var r,i=e,o=Element.prototype;o!==i;){r=il(i);for(var d in r)r[d].set&&d!=="innerHTML"&&d!=="textContent"&&d!=="innerText"&&a.push(d);i=es(i)}return a}function Mn(e,t,a=t){var r=new WeakSet;$s(e,"input",async i=>{var o=i?e.defaultValue:e.value;if(o=Ir(e)?Fr(o):o,a(o),W!==null&&r.add(W),await ho(),o!==(o=t())){var d=e.selectionStart,c=e.selectionEnd,u=e.value.length;if(e.value=o??"",c!==null){var f=e.value.length;d===c&&c===u&&f>u?(e.selectionStart=f,e.selectionEnd=f):(e.selectionStart=d,e.selectionEnd=Math.min(c,f))}}}),lr(t)==null&&e.value&&(a(Ir(e)?Fr(e.value):e.value),W!==null&&r.add(W)),_i(()=>{var i=t();if(e===document.activeElement){var o=W;if(r.has(o))return}Ir(e)&&i===Fr(e.value)||e.type==="date"&&!i&&!e.value||i!==e.value&&(e.value=i??"")})}function Ir(e){var t=e.type;return t==="number"||t==="range"}function Fr(e){return e===""?null:+e}function Kr(e,t){return e===t||(e==null?void 0:e[xa])===t}function Do(e={},t,a,r){var i=Pt.r,o=pe;return Ls(()=>{var d,c;return _i(()=>{d=c,c=[],lr(()=>{Kr(a(...c),e)||(t(e,...c),d&&Kr(a(...d),e)&&t(null,...d))})}),()=>{let u=o;for(;u!==i&&u.parent!==null&&u.parent.f&Jr;)u=u.parent;const f=()=>{c&&Kr(a(...c),e)&&t(null,...c)},g=u.teardown;u.teardown=()=>{f(),g==null||g()}}}),e}function or(e){Pt===null&&as(),gi(()=>{const t=lr(e);if(typeof t=="function")return t})}function Oo(e){Pt===null&&as(),or(()=>()=>lr(e))}let Xa=localStorage.getItem("mc_token")||"";function Ws(){return Xa}function Ro(e){Xa=(e||"").trim(),localStorage.setItem("mc_token",Xa)}function Bo(){const e={"Content-Type":"application/json"};return Xa&&(e["X-MC-Token"]=Xa),e}function Ja(){return localStorage.getItem("mc_hf_token")||""}function qo(e){localStorage.setItem("mc_hf_token",(e||"").trim())}async function fe(e,t={}){const a=await fetch(e,{headers:Bo(),...t}),r=await a.json().catch(()=>({}));if(!a.ok)throw new Error(r.error||"HTTP "+a.status);return r}const No=(e,t=document)=>t.querySelector(e),Bi=(e,t=document)=>[...t.querySelectorAll(e)];function An(e){return String(e??"").replace(/[&<>]/g,t=>({"&":"&","<":"<",">":">"})[t])}let qi;function Z(e,t=!1){const a=No("#toast");a&&(a.textContent=e,a.className="toast show"+(t?" err":""),clearTimeout(qi),qi=setTimeout(()=>a.className="toast",3200))}function Io(e,t){return e==="running"||e==="ready"?'geladen':e==="loading"||e==="starting"?'lädt…':e==="downloading"?`↓ Download${t!=null?` ${Math.round(t)}%`:""}`:'bereit'}const Ee=e=>`${e}`,Pr={logo:Ee(''),grid:Ee(''),cpu:Ee(''),pulse:Ee(''),server:Ee(''),book:Ee(''),help:Ee(''),settings:Ee(''),swap:Ee(''),monitor:Ee(''),layers:Ee(''),alert:Ee(''),gauge:Ee(''),search:Ee(''),compass:Ee(''),code:Ee(''),eye:Ee(''),refresh:Ee(''),file:Ee(''),database:Ee(''),thermo:Ee(''),shield:Ee(''),info:Ee(''),chevron:Ee(''),bolt:Ee(''),x:Ee(''),check:Ee(''),download:Ee(''),clock:Ee('')};function ea(e){return Pr[e]||""}function si(e){return`${Pr.info}`}function Xn(e){if(!e&&e!==0)return"–";const t=e/1024/1024/1024;return t>=1?t.toFixed(1)+" GB":Math.round(e/1024/1024)+" MB"}function _n({title:e,body:t,confirmLabel:a="Bestätigen",cancelLabel:r="Abbrechen",danger:i=!1}){return new Promise(o=>{const d=document.createElement("div");d.className="modal-overlay",d.innerHTML=``;const u=c.querySelector("input"),f=_=>{c.remove(),d(_)};c.addEventListener("click",_=>{if(_.target===c)return f(null);const T=_.target.closest("[data-x]");T&&f(T.getAttribute("data-x")==="1"?u.value||"":null)}),u.addEventListener("keydown",_=>{_.key==="Enter"&&f(u.value||""),_.key==="Escape"&&f(null)}),document.body.appendChild(c),u.focus()})}function Ko(e="overview"){const t=qi(".nav-item[data-view]:not(.disabled)"),a=qi(".view[data-view]"),r=new Set(a.map(o=>o.dataset.view));function i(o){r.has(o)||(o=e),a.forEach(d=>d.hidden=d.dataset.view!==o),t.forEach(d=>d.classList.toggle("active",d.dataset.view===o)),location.hash!=="#/"+o&&(location.hash="#/"+o)}return t.forEach(o=>o.addEventListener("click",d=>{d.preventDefault(),i(o.dataset.view)})),window.addEventListener("hashchange",()=>i(location.hash.replace("#/",""))),i(location.hash.replace("#/","")||e),{show:i}}let Fi=R(null);const dr={get value(){return n(Fi)},set(e){g(Fi,e,!0)}};let Ki=R(Bt([])),Kn=R(Bt([]));const ra={get value(){return n(Ki)},set(e){g(Ki,e||[],!0)},get tracked(){return n(Kn)},track(e){n(Kn).includes(e)||g(Kn,[...n(Kn),e],!0)},toggle(e){g(Kn,n(Kn).includes(e)?n(Kn).filter(t=>t!==e):[...n(Kn),e],!0)}};let Gi=R(null);const xi={get value(){return n(Gi)},set(e){g(Gi,e,!0)}},Go="5";var Xi;typeof window<"u"&&((Xi=window.__svelte??(window.__svelte={})).v??(Xi.v=new Set)).add(Go);var Uo=M('
GPU-Temperatur
Freier Speicher
GB
',1),Ho=M('
GPU-Temperatur
messe…
Freier Speicher
messe…
',1),Vo=M('
'),Wo=M('
Warte auf Messwerte…
'),jo=M(" "),Jo=M('
'),Qo=M('
'),Ui=M('
'),Yo=M('
Noch keine Modelle
Hol dir unter „Cookbook" ein Modell, das auf deine Hardware passt.
'),Zo=M('
'),Xo=M('
News gerade nicht ladbar.
'),ed=M('

LLM-Engine
llama-swap
Aktives Modell

System-Gesundheit

So ausgelastet ist dein Mini-PC gerade.

Schnellstart

Die häufigsten Aufgaben — ein Klick.

Dein Stack

Top-News

Alle →
',1);function td(e,t){Yn(t,!0);const a=q(()=>dr.value),r=q(()=>xi.value),i=q(()=>{var p;return((p=n(a))==null?void 0:p.models)||[]}),o=q(()=>{var p;return((p=n(a))==null?void 0:p.swap_ok)??!1}),d=q(()=>n(i).find(p=>["running","ready","loading","starting"].includes(p.state))),c=q(()=>n(a)?n(o)?n(r)&&n(r).ram.percent>=90?"Achtung: Speicher wird knapp.":"Alles läuft rund.":"LLM-Engine offline":"Verbinde…"),u=q(()=>n(a)?n(o)?n(r)&&n(r).ram.percent>=90?`Arbeitsspeicher bei ${Math.round(n(r).ram.percent)} % — eventuell Speicher freigeben.`:`${n(i).length} ${n(i).length===1?"Modell":"Modelle"} bereit · ${n(d)?`„${n(d).name}" geladen`:"keins geladen"} · keine Warnungen.`:"Der llama-swap-Dienst antwortet gerade nicht.":""),f=q(()=>()=>{var D;const p=(D=n(r))==null?void 0:D.gpu;return p&&p.vram.total+p.gtt.total>0?(p.vram.used+p.gtt.used)/(p.vram.total+p.gtt.total)*100:0}),_=p=>p==null?"":p<55?"kühl & gesund":p<72?"normal":"läuft heiß";let T=R(Bt([]));or(async()=>{try{g(T,((await fe("/api/news")).items||[]).slice(0,4),!0)}catch{}});async function h(){await bn({title:"Speicher freigeben?",body:"Alle aktuell geladenen Modelle werden aus dem Grafikspeicher geworfen. Sie laden beim nächsten Aufruf automatisch neu — es geht nichts verloren.",confirmLabel:"Speicher freigeben"})&&(await fe("/api/unload",{method:"POST"}),document.dispatchEvent(new Event("mc:refresh")))}function b(p){var D;(D=document.querySelector(`.nav-item[data-view="${p}"]`))==null||D.dispatchEvent(new MouseEvent("click"))}function A(p){return p!=null&&p.includes("Bild")?"img":p!=null&&p.includes("Code")?"code":"text"}function C(p){return p!=null&&p.includes("Bild")?"Bild":p!=null&&p.includes("Code")?"Code":"Text"}var S=ed(),B=ft(S),ye=s(B),ee=s(ye),ce=s(ee),_e=l(ee,2),I=s(_e),K=l(B,2),ie=s(K),ve=l(s(ie),2),me=s(ve),Me=l(ie,2),Ue=l(s(Me),2),E=s(Ue),Q=l(Ue,2),de=s(Q),Ae=l(Me,2);{var Y=p=>{const D=q(()=>{var wt;return n(r).gpu_temp??((wt=n(r).cpu)==null?void 0:wt.temp)??null}),se=q(()=>n(r).ram.total-n(r).ram.used);var j=Uo(),xe=ft(j),ge=l(s(xe),2),it=s(ge),Je=l(ge,2),dt=s(Je),Ve=l(xe,2),yt=l(s(Ve),2),Tt=s(yt),Rt=l(yt,2),Gt=s(Rt);O((wt,Ut,Ee,Be)=>{m(it,wt),lt(Je,1,`t-s ${n(D)!=null&&n(D)<72?"ok":""}`),m(dt,Ut),m(Tt,Ee),m(Gt,`von ${Be??""} GB`)},[()=>n(D)!=null?`${Math.round(n(D))}°`:"–",()=>_(n(D)),()=>Math.round(n(se)/1073741824),()=>Math.round(n(r).ram.total/1073741824)]),w(p,j)},be=p=>{var D=Ho();w(p,D)};V(Ae,p=>{n(r)?p(Y):p(be,-1)})}var Z=l(K,2),ze=s(Z),Ce=l(s(ze),4);{var He=p=>{const D=q(()=>n(f)());var se=Lr(),j=ft(se);Ge(j,17,()=>{var xe;return[["Prozessor (CPU)",((xe=n(r).cpu)==null?void 0:xe.percent)??0],["Arbeitsspeicher (RAM)",n(r).ram.percent],["Grafikspeicher (VRAM)",n(D)]]},Ke,(xe,ge)=>{var it=q(()=>Ya(n(ge),2));let Je=()=>n(it)[0],dt=()=>n(it)[1];const Ve=q(()=>Math.max(0,Math.min(100,dt()||0)));var yt=Vo(),Tt=s(yt),Rt=s(Tt),Gt=s(Rt),wt=l(Rt),Ut=s(wt),Ee=l(Tt,2),Be=s(Ee);O(($t,sn)=>{m(Gt,Je()),m(Ut,`${$t??""} %`),lt(Ee,1,`bar ${n(Ve)>=90?"bad":n(Ve)>=75?"warn":""}`),Bn(Be,`width:${sn??""}%`)},[()=>Math.round(n(Ve)),()=>Math.max(2,n(Ve))]),w(xe,yt)}),w(p,se)},Ie=p=>{var D=Wo();w(p,D)};V(Ce,p=>{n(r)?p(He):p(Ie,-1)})}var Re=l(ze,2),_t=l(s(Re),4),mt=s(_t);Ye(mt,()=>ea("search"),!0);var Kt=l(mt,4);Ye(Kt,()=>ea("chevron"),!0);var St=l(_t,2),Et=s(St);Ye(Et,()=>ea("refresh"),!0);var P=l(Et,4);Ye(P,()=>ea("chevron"),!0);var N=l(St,2),J=s(N);Ye(J,()=>ea("file"),!0);var re=l(J,4);Ye(re,()=>ea("chevron"),!0);var x=l(Z,2),L=s(x),G=s(L),te=l(s(G),2),$=s(te),H=l(G,2);{var ne=p=>{var D=Ui();Ge(D,21,()=>n(i),Ke,(se,j)=>{const xe=q(()=>n(j).state==="downloading"),ge=q(()=>["running","ready","loading","starting"].includes(n(j).state)),it=q(()=>n(j).state==="loading"||n(j).state==="starting"||n(xe)?"load":n(ge)?"on":""),Je=q(()=>n(xe)?`↓ Download${n(j).download_progress!=null?` ${Math.round(n(j).download_progress)}%`:""}`:n(ge)?n(j).state==="loading"?"lädt…":"geladen":"bereit");var dt=Qo(),Ve=s(dt),yt=l(Ve,2),Tt=s(yt),Rt=s(Tt),Gt=s(Rt),wt=l(Rt,2);{var Ut=ln=>{var xt=jo(),kn=s(xt);O((Pe,At)=>{lt(xt,1,`tag ${Pe??""}`),m(kn,At)},[()=>A(n(j).meta.caps),()=>C(n(j).meta.caps)]),w(ln,xt)};V(wt,ln=>{var xt,kn;(kn=(xt=n(j).meta)==null?void 0:xt.caps)!=null&&kn.length&&ln(Ut)})}var Ee=l(Tt,2);{var Be=ln=>{var xt=Jo(),kn=s(xt);O(()=>m(kn,n(j).meta.filename)),w(ln,xt)};V(Ee,ln=>{var xt;(xt=n(j).meta)!=null&&xt.filename&&ln(Be)})}var $t=l(yt,2),sn=s($t);O(()=>{lt(Ve,1,`li-dot ${n(it)??""}`),m(Gt,n(j).name),m(sn,n(Je))}),w(se,dt)}),w(p,D)},he=p=>{var D=Yo();w(p,D)};V(H,p=>{n(i).length?p(ne):p(he,-1)})}var Ne=l(L,2),Se=s(Ne),je=l(s(Se),2),rt=l(Se,2);{var bt=p=>{var D=Ui();Ge(D,21,()=>n(T),Ke,(se,j)=>{var xe=Zo(),ge=s(xe),it=s(ge),Je=s(it),dt=l(it,2),Ve=s(dt);O(()=>{zr(xe,"href",n(j).link),m(Je,n(j).title),m(Ve,n(j).source)}),w(se,xe)}),w(p,D)},Ze=p=>{var D=Xo();w(p,D)};V(rt,p=>{n(T).length?p(bt):p(Ze,-1)})}O(()=>{m(ce,n(c)),m(I,n(u)),lt(ve,1,`t-v ${n(o)?"ok":"bad"}`),m(me,n(o)?"Online":"Offline"),m(E,n(d)?n(d).name:"Keins"),m(de,n(d)?"im Grafikspeicher":"nichts geladen"),m($,n(i).length?n(i).length+" konfiguriert":"")}),z("click",_t,()=>b("cookbook")),z("click",St,h),z("click",N,()=>b("server")),z("click",je,()=>b("news")),z("keydown",je,p=>p.key==="Enter"&&b("news")),w(e,S),Zn()}_a(["click","keydown"]);var nd=M(""),ad=M('
'),rd=M('
Noch keine Modelle konfiguriert
Hol dir unter „Cookbook" ein passendes Modell.
'),id=M('
kein Modell hinterlegt
leere Rolleleer '),sd=M(' '),ld=M('
'),od=M(' '),dd=M('
API-Name:
'),cd=M('
ModellKannDetailsStatusPortAktionen
'),ud=M('
Empfohlen für deine Hardware

'),vd=M(''),fd=M(''),pd=M(''),hd=M('

Modelle

Deine konfigurierten Modelle — testen, Kontext anpassen oder aus dem Speicher werfen.

Schnelltest

Schreib eine Nachricht — das gewählte Modell wird automatisch geweckt.

Modelle & Ports

Modelle laden automatisch, sobald eine Anfrage kommt — du musst nichts manuell starten.
',1);function gd(e,t){Yn(t,!0);const a="Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr (längere Dateien/Chats), braucht aber mehr Speicher und wird etwas langsamer.",r="Die Rolle ist ein zusätzlicher, sprechender Name (z.B. coder). Du kannst in deinen Tools entweder den echten Modellnamen ODER die Rolle angeben — beides führt zum selben Modell.",i=[{id:"coder",label:"Coder",desc:"Programmieren & Code"},{id:"vision",label:"Vision",desc:"Bilder verstehen"},{id:"scout",label:"Scout",desc:"Schneller Allrounder"},{id:"reviewer",label:"Reviewer",desc:"Code/Texte prüfen"},{id:"manager",label:"Manager",desc:"Planen & koordinieren"}],o=q(()=>{var x;return((x=dr.value)==null?void 0:x.models)||[]});function d(){document.dispatchEvent(new Event("mc:refresh"))}let c=R(""),u=R(""),f=R(""),_=R(!1);const T=q(()=>n(o).filter(x=>!x.incomplete));async function h(){var L;const x=n(c)||((L=n(T)[0])==null?void 0:L.name);if(!(!x||!n(u).trim())){g(_,!0),g(f,"(wecke Modell, kann beim Laden kurz dauern…)");try{const G=await fe("/api/chat",{method:"POST",body:JSON.stringify({model:x,message:n(u)})});g(f,G.reply,!0)}catch(G){g(f,"Fehler: "+G.message)}g(_,!1),d()}}const b=300;let A=R(!1),C=R(null),S=R(8192),B=R(!1);function ye(x){var L;g(C,n(o).find(G=>G.name===x),!0),n(C)&&(g(S,((L=n(C).meta)==null?void 0:L.ctx)||8192,!0),g(B,(n(C).ttl??b)>=99999),g(A,!0))}async function ee(){const x=n(B)?99999:b;try{await fe("/api/update_model",{method:"POST",body:JSON.stringify({alias:n(C).name,ctx:n(S),ttl:x})}),X("Gespeichert — aktiv beim nächsten Modell-Start."),g(A,!1),d()}catch(L){X(L.message,!0)}}let ce=R(!1),_e=R(""),I=R(""),K=R("");function ie(x){const L=n(o).find(G=>G.name===x);g(_e,x,!0),g(K,(L==null?void 0:L.role)||"",!0),g(I,""),g(ce,!0)}async function ve(x){try{await fe("/api/set_role",{method:"POST",body:JSON.stringify({alias:n(_e),role:x})}),X(x?`Rolle gesetzt: ${x}`:"Rolle entfernt."),g(ce,!1),d()}catch(L){X(L.message,!0)}}async function me(x){if(await bn({title:`„${x}" entladen?`,body:"Das Modell wird aus dem Grafikspeicher geworfen und lädt beim nächsten Aufruf automatisch neu.",confirmLabel:"Entladen"}))try{await fe("/api/unload?model="+encodeURIComponent(x),{method:"POST"}),X("Entladen: "+x),setTimeout(d,600)}catch(L){X(L.message,!0)}}async function Me(x){if(await bn({title:`„${x}" wirklich löschen?`,body:"Das Modell wird aus der Konfiguration ausgetragen und seine Dateien werden von der Festplatte gelöscht, um Speicher freizugeben. Das lässt sich nicht rückgängig machen — später brauchst du dafür einen neuen Download.",confirmLabel:"Endgültig löschen",danger:!0}))try{const L=await fe("/api/delete_model",{method:"POST",body:JSON.stringify({alias:x})});X(L.note||"Gelöscht: "+x),setTimeout(d,600)}catch(L){X(L.message,!0)}}function Ue(x){return x!=null&&x.length?x.map(L=>L==="Code"?'Code':L==="Bild"?'Bild':'Text').join(" "):"–"}function E(x){if(!x)return"–";const L=x.quant||"?",G=x.ctx?Math.round(x.ctx/1024)+"K":"?",te=x.size_bytes?(x.size_bytes/1024**3).toFixed(1)+" GB":"?";return`${L} · ${G} · ${te}`}function Q(x){var L;(L=navigator.clipboard)==null||L.writeText(x),X("Kopiert: "+x)}var de=hd(),Ae=l(ft(de),2),Y=l(s(Ae),6);Ge(Y,21,()=>n(T),Ke,(x,L)=>{var G=nd(),te=s(G),$={};O(()=>{m(te,n(L).name),$!==($=n(L).name)&&(G.value=(G.__value=n(L).name)??"")}),w(x,G)});var be=l(Y,4),Z=l(be,2),ze=s(Z),Ce=l(Z,2);{var He=x=>{var L=ad(),G=s(L);O(()=>m(G,n(f))),w(x,L)};V(Ce,x=>{n(f)&&x(He)})}var Ie=l(Ae,2),Re=s(Ie),_t=l(s(Re)),mt=s(_t),Kt=l(Re,4);{var St=x=>{var L=rd();w(x,L)},Et=x=>{var L=cd(),G=l(s(L));Ge(G,21,()=>n(o),Ke,(te,$)=>{var H=Lr(),ne=ft(H);{var he=Se=>{var je=id(),rt=s(je),bt=s(rt),Ze=l(rt,5),p=s(Ze),D=l(p,2);O(()=>m(bt,n($).name)),z("click",p,()=>{var se;X("Lade im Cookbook ein Modell und setze den Alias auf: "+n($).name),(se=document.querySelector(".nav-item[data-view='cookbook']"))==null||se.dispatchEvent(new MouseEvent("click"))}),z("click",D,()=>Me(n($).name)),w(Se,je)},Ne=Se=>{var je=dd(),rt=s(je),bt=s(rt),Ze=s(bt),p=l(Ze);{var D=Ee=>{var Be=sd(),$t=s(Be);O(()=>m($t,n($).role)),w(Ee,Be)};V(p,Ee=>{n($).role&&Ee(D)})}var se=l(bt,2);{var j=Ee=>{var Be=ld(),$t=s(Be);O(()=>m($t,n($).meta.filename)),w(Ee,Be)};V(se,Ee=>{var Be;(Be=n($).meta)!=null&&Be.filename&&Ee(j)})}var xe=l(se,2),ge=l(s(xe),2);Ge(ge,17,()=>{var Ee;return(Ee=n($).api_ids)!=null&&Ee.length?n($).api_ids:[n($).name]},Ke,(Ee,Be)=>{var $t=od(),sn=s($t);O(()=>m(sn,n(Be))),z("click",$t,()=>Q(n(Be))),w(Ee,$t)});var it=l(rt);Ye(it,()=>{var Ee;return Ue((Ee=n($).meta)==null?void 0:Ee.caps)},!0);var Je=l(it);Ye(Je,()=>E(n($).meta),!0);var dt=l(Je);Ye(dt,()=>Fo(n($).state,n($).download_progress),!0);var Ve=l(dt),yt=s(Ve),Tt=l(Ve),Rt=s(Tt),Gt=l(Rt,2),wt=l(Gt,2),Ut=l(wt,2);O(()=>{m(Ze,`${n($).name??""} `),m(yt,n($).port??"auto")}),z("click",Rt,()=>ie(n($).name)),z("click",Gt,()=>ye(n($).name)),z("click",wt,()=>me(n($).name)),z("click",Ut,()=>Me(n($).name)),w(Se,je)};V(ne,Se=>{n($).incomplete?Se(he):Se(Ne,-1)})}w(te,H)}),w(x,L)};V(Kt,x=>{n(o).length?x(Et,-1):x(St)})}var P=l(Ie,2);{var N=x=>{var L=vd(),G=s(L),te=s(G),$=l(te,4),H=s($),ne=l($,2),he=l(s(ne));Ye(he,()=>li(a));var Ne=l(ne,2),Se=l(Ne,2);{var je=j=>{var xe=ud(),ge=s(xe),it=l(s(ge),2),Je=s(it),dt=l(Je,2),Ve=l(ge,2);O((yt,Tt)=>{m(Je,`Optimal ~${yt??""}k → ~${n(C).meta.peak_ram_optimal_gb??""} GB Spitzenbedarf.`),m(dt,` Aktuell ~${Tt??""}k → ~${n(C).meta.peak_ram_gb??""} GB.`)},[()=>Math.round(n(C).meta.optimal_ctx/1024),()=>Math.round((n(C).meta.ctx||0)/1024)]),z("click",Ve,()=>g(S,n(C).meta.optimal_ctx,!0)),w(j,xe)};V(Se,j=>{var xe;(xe=n(C).meta)!=null&&xe.optimal_ctx&&j(je)})}var rt=l(Se,4),bt=l(s(rt),2),Ze=s(bt),p=l(Ze,2),D=s(p),se=l(rt,2);O(()=>{m(H,n(C).name),Bn(p,`color:${n(B)?"var(--accent)":"var(--mut)"}`),m(D,n(B)?"An (TTL = ∞)":"Aus (TTL = 5 min)")}),z("click",L,j=>{j.target.classList.contains("modal-overlay")&&g(A,!1)}),z("click",te,()=>g(A,!1)),Sn(Ne,()=>n(S),j=>g(S,j)),Do(Ze,()=>n(B),j=>g(B,j)),z("click",se,ee),w(x,L)};V(P,x=>{n(A)&&n(C)&&x(N)})}var J=l(P,2);{var re=x=>{var L=pd(),G=s(L),te=s(G),$=l(te,2),H=l(s($));Ye(H,()=>li(r));var ne=l($,2),he=s(ne),Ne=l(ne,4);Ge(Ne,21,()=>i,Ke,(Ze,p)=>{var D=fd(),se=s(D);O(()=>{lt(D,1,n(K)===n(p).id?"primary":"ghost"),zr(D,"title",n(p).desc),m(se,n(p).label)}),z("click",D,()=>ve(n(p).id)),w(Ze,D)});var Se=l(Ne,4),je=l(Se,2),rt=s(je),bt=l(rt,2);O(()=>m(he,n(_e))),z("click",L,Ze=>{Ze.target.classList.contains("modal-overlay")&&g(ce,!1)}),z("click",te,()=>g(ce,!1)),Sn(Se,()=>n(I),Ze=>g(I,Ze)),z("click",rt,()=>ve("")),z("click",bt,()=>ve(n(I).trim().toLowerCase())),w(x,L)};V(J,x=>{n(ce)&&x(re)})}O(()=>{Z.disabled=n(_),m(ze,n(_)?"…":"Senden"),m(mt,n(o).length?n(o).length+" konfiguriert":"")}),ja(Y,()=>n(c),x=>g(c,x)),Sn(be,()=>n(u),x=>g(u,x)),z("click",Z,h),w(e,de),Zn()}_a(["click"]);var _d=M('
Prozessor (CPU)
%
Arbeitsspeicher
%
Grafikspeicher
%

System-Metriken (Bosgame)

Live-Auslastung deines Mini-PCs, alle 0,5 Sekunden.
Arbeitsspeicher (RAM)
Grafikspeicher (VRAM + GTT)
Speicherplatz (Disk)
Temperatur (GPU / CPU)
CPU-Verlauf
RAM-Verlauf
VRAM-Verlauf
',1),md=M(''),bd=M('
Gerade nichts los.
Alles ruhig — keine laufenden Aufgaben.
'),yd=M(' '),wd=M(''),xd=M(''),kd=M('
'),Md=M('
'),Sd=M('
'),Ed=M('

Aktivität

Live-Auslastung und laufende Aufgaben (Downloads, Updates) mit Protokoll.

Hintergrund-Aufgaben

Downloads & Updates erscheinen hier mit Live-Protokoll — zum Aufklappen klicken.
',1);function Td(e,t){Yn(t,!0);const a=q(()=>ra.value),r=q(()=>ra.tracked),i=q(()=>xi.value);let o=Bt({cpu:[],ram:[],gpu:[]});mi(()=>{var de;if(!n(i))return;const E=n(i).gpu,Q=E&&E.vram.total+E.gtt.total>0?(E.vram.used+E.gtt.used)/(E.vram.total+E.gtt.total)*100:0;o.cpu=[...o.cpu,((de=n(i).cpu)==null?void 0:de.percent)??0].slice(-60),o.ram=[...o.ram,n(i).ram.percent].slice(-60),o.gpu=[...o.gpu,Q].slice(-60)});const d=q(()=>()=>{var Q;const E=(Q=n(i))==null?void 0:Q.gpu;return E&&E.vram.total+E.gtt.total>0?(E.vram.used+E.gtt.used)/(E.vram.total+E.gtt.total)*100:0}),c=q(()=>n(a).filter(E=>E.state!=="running"&&E.state!=="queued").length),u=q(()=>n(a).filter(E=>E.state==="failed").length);function f(E){if(E.state!=="running")return null;if(typeof E.progress=="number")return Math.min(100,E.progress);const de=((E.log||[]).at(-1)||"").match(/(\d+(?:\.\d+)?)\s*%/);return de?Math.min(100,parseFloat(de[1])):null}function _(E){return E==null||E<0?"":E>=90?`noch ~${Math.round(E/60)} min`:`noch ~${Math.max(1,Math.round(E))} s`}function T(E){return E==="done"?'fertig':E==="failed"?'fehler':E==="canceled"?'abgebrochen':'läuft…'}function h(E){return E==="done"?"on":E==="failed"||E==="canceled"?"":"load"}async function b(E){try{await fe(`/api/jobs/${E}/cancel`,{method:"POST"}),X("Abbruch angefordert…")}catch(Q){X(Q.message,!0)}}async function A(E){try{await fe(`/api/jobs/${E}`,{method:"DELETE"})}catch(Q){X(Q.message,!0)}}async function C(){try{const E=await fe("/api/jobs/clear",{method:"POST"});X(`${E.removed} Einträge entfernt.`)}catch(E){X(E.message,!0)}}function S(E,Q){return E.map(de=>`
`).join("")}var B=Ed(),ye=l(ft(B),2);{var ee=E=>{const Q=q(()=>n(d)());var de=_d(),Ae=ft(de),Y=s(Ae),be=l(s(Y),2),Z=s(be),ze=l(be,2),Ce=s(ze),He=l(Y,2),Ie=l(s(He),2),Re=s(Ie),_t=l(Ie,2),mt=s(_t),Kt=l(He,2),St=l(s(Kt),2),Et=s(St),P=l(St,2),N=s(P),J=l(Ae,2),re=l(s(J),4),x=s(re),L=l(s(x)),G=s(L),te=l(x,2),$=l(s(te),2),H=s($),ne=l(te,2),he=l(s(ne)),Ne=s(he),Se=l(ne,2),je=l(s(Se),2),rt=s(je),bt=l(re,2),Ze=s(bt),p=l(s(Ze));Ye(p,()=>S(o.cpu,"--accent"),!0);var D=l(Ze,2),se=l(s(D));Ye(se,()=>S(o.ram,"--purple"),!0);var j=l(D,2),xe=l(s(j));Ye(xe,()=>S(o.gpu,"--on"),!0),O((ge,it,Je,dt,Ve,yt,Tt,Rt,Gt,wt,Ut,Ee,Be)=>{m(Z,ge),m(Ce,it),m(Re,Je),m(mt,`${dt??""} / ${Ve??""}`),m(Et,yt),m(N,Tt),m(G,`${Rt??""} / ${Gt??""}`),m(H,wt),m(Ne,`${Ut??""} % belegt`),m(rt,`${Ee??""} / ${Be??""}`)},[()=>{var ge;return Math.round(((ge=n(i).cpu)==null?void 0:ge.percent)??0)},()=>{var ge;return((ge=n(i).cpu)==null?void 0:ge.temp)!=null?`${Math.round(n(i).cpu.temp)}° CPU-Temp`:"Auslastung"},()=>Math.round(n(i).ram.percent),()=>Xn(n(i).ram.used),()=>Xn(n(i).ram.total),()=>Math.round(n(Q)),()=>n(i).gpu_temp!=null?`${Math.round(n(i).gpu_temp)}° GPU-Temp`:"VRAM + GTT",()=>Xn(n(i).ram.used),()=>Xn(n(i).ram.total),()=>n(i).gpu?`${Xn(n(i).gpu.vram.used+n(i).gpu.gtt.used)} / ${Xn(n(i).gpu.vram.total+n(i).gpu.gtt.total)}`:"–",()=>{var ge;return Math.round(((ge=n(i).disk)==null?void 0:ge.percent)??0)},()=>n(i).gpu_temp!=null?Math.round(n(i).gpu_temp)+"°":"–",()=>{var ge;return((ge=n(i).cpu)==null?void 0:ge.temp)!=null?Math.round(n(i).cpu.temp)+"°":"–"}]),w(E,de)};V(ye,E=>{n(i)&&E(ee)})}var ce=l(ye,2),_e=s(ce),I=l(s(_e),2),K=s(I),ie=l(I,2);{var ve=E=>{var Q=md();z("click",Q,C),w(E,Q)};V(ie,E=>{n(c)&&E(ve)})}var me=l(_e,4);{var Me=E=>{var Q=bd();w(E,Q)},Ue=E=>{var Q=Lr(),de=ft(Q);Ge(de,17,()=>n(a),Ke,(Ae,Y)=>{const be=q(()=>f(n(Y))),Z=q(()=>n(Y).state==="running"?_(n(Y).eta_s):""),ze=q(()=>n(Y).state==="running"&&n(Y).rate_bps?Xn(n(Y).rate_bps)+"/s":""),Ce=q(()=>[n(Z),n(ze)].filter(Boolean).join(" · ")),He=q(()=>n(r).includes(n(Y).id));var Ie=Sd(),Re=s(Ie),_t=s(Re),mt=l(_t,2),Kt=s(mt),St=l(mt,2);{var Et=$=>{var H=yd(),ne=s(H);O(he=>m(ne,`${he??""} %${n(Ce)?` · ${n(Ce)}`:""}`),[()=>Math.round(n(be))]),w($,H)};V(St,$=>{n(be)!=null&&$(Et)})}var P=l(St,2);{var N=$=>{var H=wd();O(()=>Bn(H,`${n(be)==null?"margin-left:auto;":""}margin-right:2px;padding:2px 9px;font-size:12px`)),z("click",H,ne=>{ne.stopPropagation(),b(n(Y).id)}),w($,H)},J=$=>{var H=xd();z("click",H,ne=>{ne.stopPropagation(),A(n(Y).id)}),w($,H)};V(P,$=>{n(Y).state==="running"?$(N):$(J,-1)})}var re=l(P,2);Ye(re,()=>T(n(Y).state));var x=l(Re,2);{var L=$=>{var H=kd(),ne=s(H);O(()=>Bn(ne,`width:${n(be)??""}%`)),w($,H)};V(x,$=>{n(be)!=null&&n(Y).state==="running"&&$(L)})}var G=l(x,2);{var te=$=>{var H=Md(),ne=s(H);O(he=>m(ne,he),[()=>(n(Y).log||[]).join(` +`)]),w($,H)};V(G,$=>{n(He)&&$(te)})}O($=>{lt(_t,1,`li-dot ${$??""}`),m(Kt,n(Y).label)},[()=>h(n(Y).state)]),z("click",Re,()=>ra.toggle(n(Y).id)),z("keydown",Re,$=>$.key==="Enter"&&ra.toggle(n(Y).id)),w(Ae,Ie)}),w(E,Q)};V(me,E=>{n(a).length?E(Ue,-1):E(Me)})}O(()=>m(K,n(a).length?n(u)?n(u)+" Fehler":n(a).length+" gesamt":"")),w(e,B),Zn()}_a(["click","keydown"]);var $d=M(' '),Ad=M(''),Cd=M('✓ fertig'),Ld=M('✗ fehlgeschlagen'),zd=M('abgebrochen'),Pd=M('läuft…'),Dd=M('
'),Od=M('

Aktueller Vorgang

'),Rd=M('

Server & Wartung

Dienste steuern, Updates einspielen und live mitlesen — ganz ohne SSH oder Terminal.

LLM-Engine (llama-swap)

Das Herzstück: lädt bei jeder Anfrage automatisch das passende Modell.
Adresse (für deine Tools)
Aktuell geladen
Konfigurierte Modelle

Dienste & Applikation

Diese Aktionen sind harmlos und passwortlos — es geht nichts dabei verloren.

Betriebssystem (Bosgame)

Tiefe Eingriffe — das Dashboard fragt einmalig nach deinem sudo-Passwort.

Live-Konsole

Live mitlesen, was der Dienst gerade tut.
',1);function Bd(e,t){Yn(t,!0);const a=q(()=>dr.value),r=q(()=>ra.value);let i=null,o=R(`Verbinde… +`),d=R("llama-swap"),c,u=R(null),f=R("");const _=new Set,T=q(()=>n(u)?n(r).find(p=>p.id===n(u)):null);mi(()=>{if(!n(T))return;const p=n(T),D=p.state==="done",se=p.state==="failed",j=p.state==="canceled";(D||se||j)&&!_.has(p.id)&&(_.add(p.id),X(D?`✓ ${n(f)} abgeschlossen.`:j?`${n(f)} abgebrochen.`:`✗ ${n(f)} fehlgeschlagen.`,se),D&&setTimeout(()=>document.dispatchEvent(new Event("mc:refresh")),800))});function h(p,D){g(u,p,!0),g(f,D,!0),ra.track(p)}or(()=>{b()}),Ro(()=>{i==null||i.close()});function b(){i==null||i.close();const p=n(d);g(o,`Verbinde mit ${p}… +`);const D=location.protocol==="https:"?"wss:":"ws:";i=new WebSocket(`${D}//${location.host}/api/logs/${p}?token=${encodeURIComponent(Ws())}`),i.onmessage=se=>{g(o,n(o)+se.data),c&&(c.scrollTop=c.scrollHeight)},i.onclose=()=>{g(o,n(o)+` +— Verbindung getrennt —`)},i.onerror=()=>i==null?void 0:i.close()}async function A(p,D,se){if(await bn({title:D,body:se,confirmLabel:"Neustarten"}))try{await fe(`/api/service/${p}/restart`,{method:"POST"}),X("Neustart ausgelöst: "+p),setTimeout(()=>document.dispatchEvent(new Event("mc:refresh")),2e3)}catch(j){X(j.message,!0)}}async function C(){if(await bn({title:"Mission Control aktualisieren?",body:"Holt die neueste Dashboard-Version aus Gitea und startet automatisch neu. Die Seite trennt kurz und verbindet von selbst wieder (~10 Sek)."}))try{const p=await fe("/api/self-update",{method:"POST"});X("Update läuft — Dashboard startet gleich neu."),h(p.job_id,"Mission Control Update")}catch(p){X(p.message,!0)}}async function S(){if(await bn({title:"LLM-Engine aktualisieren?",body:"Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar."}))try{const p=await fe("/api/update",{method:"POST"});X("LLM-Engine-Update läuft."),h(p.job_id,"LLM-Engine-Update")}catch(p){X(p.message,!0)}}async function B(){if(await bn({title:"Grafikspeicher leeren?",body:"Alle geladenen Modelle werden entladen. Sie laden beim nächsten Aufruf automatisch neu — es geht nichts verloren.",confirmLabel:"Leeren"}))try{await fe("/api/unload",{method:"POST"}),X("Grafikspeicher geleert."),setTimeout(()=>document.dispatchEvent(new Event("mc:refresh")),600)}catch(p){X(p.message,!0)}}async function ye(){if(!await bn({title:"OS-Updates installieren?",body:"Führt apt update & upgrade aus. Das kann ein paar Minuten dauern; der Fortschritt erscheint in der Aktivität."}))return;const p=await Ni({title:"sudo-Passwort",body:"Für die System-Updates wird einmalig dein sudo-Passwort gebraucht.",placeholder:"sudo-Passwort",password:!0,confirmLabel:"Installieren"});if(p)try{const D=await fe("/api/os-update",{method:"POST",body:JSON.stringify({password:p})});X("OS-Update gestartet."),h(D.job_id,"OS-Update")}catch(D){X(D.message,!0)}}async function ee(){if(!await bn({title:"Server wirklich neu starten?",body:"Der ganze Bosgame startet physisch neu. Alles ist für ~1 Minute offline — auch dieses Dashboard.",confirmLabel:"Reboot",danger:!0}))return;const p=await Ni({title:"sudo-Passwort",body:"Für den Reboot wird einmalig dein sudo-Passwort gebraucht.",placeholder:"sudo-Passwort",password:!0,confirmLabel:"Jetzt neustarten",danger:!0});if(p)try{await fe("/api/reboot",{method:"POST",body:JSON.stringify({password:p})}),X("Reboot ausgelöst — bis gleich.")}catch(D){X(D.message,!0)}}async function ce(){g(_e,"prüfe…");try{const p=await fe("/api/integration/test");g(_e,p.ok?`✓ Engine antwortet — ${p.models.length} Modell(e): ${p.models.join(", ")}`:`✗ keine Verbindung: ${p.error}`,!0),g(I,p.ok,!0)}catch(p){g(_e,"✗ "+p.message),g(I,!1)}}let _e=R(""),I=R(!1);const K=q(()=>{var p;return((p=n(a))==null?void 0:p.models)||[]}),ie=q(()=>n(K).find(p=>p.state==="running"||p.state==="loading"));function ve(p){if(!p||p.state!=="running")return null;if(typeof p.progress=="number")return Math.min(100,p.progress);const se=((p.log||[]).at(-1)||"").match(/(\d+(?:\.\d+)?)\s*%/);return se?Math.min(100,parseFloat(se[1])):null}var me=Rd(),Me=l(ft(me),2),Ue=s(Me),E=l(s(Ue),2),Q=s(E),de=l(Ue,4),Ae=s(de),Y=l(s(Ae)),be=s(Y),Z=l(Ae,2),ze=l(s(Z),2),Ce=s(ze),He=l(Z,2),Ie=l(s(He),2),Re=s(Ie),_t=l(de,2),mt=s(_t),Kt=l(mt,2);{var St=p=>{var D=$d(),se=s(D);O(()=>{Bn(D,`color:${n(I)?"#7ee29a":"#ff9b95"}`),m(se,n(_e))}),w(p,D)};V(Kt,p=>{n(_e)&&p(St)})}var Et=l(Me,2),P=l(s(Et),4),N=l(P,2),J=l(N,2),re=l(J,2),x=l(re,2),L=l(Et,2),G=l(s(L),4),te=l(G,2),$=l(L,2);{var H=p=>{const D=q(()=>n(T)),se=q(()=>n(D).state==="done"),j=q(()=>n(D).state==="failed"),xe=q(()=>n(D).state==="canceled"),ge=q(()=>n(se)||n(j)||n(xe)),it=q(()=>ve(n(D)));var Je=Od(),dt=s(Je),Ve=l(s(dt),2);{var yt=Pe=>{var At=Ad();z("click",At,async()=>{await bn({title:n(f)+" abbrechen?",body:"Der laufende Vorgang wird gestoppt.",confirmLabel:"Abbrechen",danger:!0})&&fe(`/api/jobs/${n(D).id}/cancel`,{method:"POST"})}),w(Pe,At)};V(Ve,Pe=>{n(ge)||Pe(yt)})}var Tt=l(Ve,2),Rt=s(Tt);{var Gt=Pe=>{var At=Cd();w(Pe,At)},wt=Pe=>{var At=Ld();w(Pe,At)},Ut=Pe=>{var At=zd();w(Pe,At)},Ee=Pe=>{var At=Pd();w(Pe,At)};V(Rt,Pe=>{n(se)?Pe(Gt):n(j)?Pe(wt,1):n(xe)?Pe(Ut,2):Pe(Ee,-1)})}var Be=l(dt,2),$t=s(Be),sn=l(Be,2);{var ln=Pe=>{var At=Dd(),Or=s(At);O(()=>Bn(Or,`width:${n(it)??""}%`)),w(Pe,At)};V(sn,Pe=>{n(it)!=null&&!n(ge)&&Pe(ln)})}var xt=l(sn,2),kn=s(xt);O(Pe=>{m($t,`${n(f)??""}${n(ge)?n(se)?" — erfolgreich.":n(xe)?" — abgebrochen.":" — fehlgeschlagen.":" läuft…"}`),m(kn,Pe)},[()=>(n(D).log||[]).join(` +`)]),w(p,Je)};V($,p=>{n(T)&&p(H)})}var ne=l($,2),he=s(ne),Ne=l(s(he),2),Se=s(Ne);Se.value=Se.__value="llama-swap";var je=l(Se);je.value=je.__value="mission-control";var rt=l(je);rt.value=rt.__value="system";var bt=l(he,4),Ze=s(bt);Oo(bt,p=>c=p,()=>c),O(p=>{var D,se,j;Bn(E,`margin-left:auto;${(D=n(a))!=null&&D.swap_ok?"background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a":"background:rgba(248,81,73,.12);border-color:rgba(248,81,73,.3);color:#ff9b95"}`),m(Q,(se=n(a))!=null&&se.swap_ok?"● erreichbar":"● offline"),m(be,(((j=n(a))==null?void 0:j.swap_url)||"—")+"/v1"),m(Ce,n(ie)?n(ie).name+` (${n(ie).state})`:"nichts geladen — lädt bei der nächsten Anfrage"),m(Re,p),m(Ze,n(o))},[()=>n(K).filter(p=>!p.incomplete).length||"keine"]),z("click",mt,ce),z("click",P,()=>A("llama-swap","LLM-Engine neustarten?","Die Engine startet neu. Geladene Modelle werden kurz entladen (~5 Sekunden), laden danach automatisch wieder.")),z("click",N,()=>A("mission-control","Dashboard neustarten?","Diese Oberfläche trennt sich kurz und verbindet automatisch wieder.")),z("click",J,C),z("click",re,S),z("click",x,B),z("click",G,ye),z("click",te,ee),z("change",Ne,b),ja(Ne,()=>n(d),p=>g(d,p)),w(e,me),Zn()}_a(["click","change"]);var qd=M('
Lade Setups…
'),Id=M(' ×',1),Ur=M('
★ Beste Wahl für dein System
'),Nd=M('
Dein Setup
'),Fd=M(''),Kd=M('
'),Gd=M('
Frage Quellen ab…
'),Ud=M('
'),Hd=M('✓ installiert'),Vd=M(''),Wd=M('

'),jd=M('

'),Jd=M(''),Qd=M('

'),Yd=M('
'),Zd=M('
'),Xd=M('
'),ec=M(" "),tc=M('
'),nc=M('
Größter Spitzenbedarf: . Es läuft immer nur ein Modell gleichzeitig — das größte bestimmt, ob das Setup passt.
'),ac=M(''),rc=M('
Lade Dateien von HuggingFace…
'),Hi=M(""),ic=M('
Empfohlener Kontext für deine Hardware: übernehmen
'),sc=M('
Ressourcen-Check
'),lc=M('
',1),oc=M('
Keine GGUF-Dateien im Repo gefunden.
'),dc=M(''),cc=M('
'),uc=M(''),vc=M('

Cookbook

Modelle finden, installieren und eigene Setups zusammenstellen.

Use-Case-Setups

Fertige Modell-Bundles für deinen Anwendungsfall. Hardware-Ampel zeigt, ob es auf deinen PC passt.

Aktuell die besten Modelle für dein System

Profi-Suche (HuggingFace)

Direkt HuggingFace durchsuchen — oder eine Modell-URL einfügen für den direkten Sprung.
',1);function fc(e,t){Yn(t,!0);const a="Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr, braucht aber mehr Speicher und wird etwas langsamer.",r="MoE (Mixture of Experts): viele Parameter fürs Wissen, aber pro Wort ist nur ein kleiner Teil aktiv → fast große-Modell-Qualität bei hohem Tempo. Ideal für deine APU.",i={perfect:0,marginal:1,too_tight:2},o={vision:"Bilder",coder:"Coden",reasoning:"Logik",agent:"Agenten & Tools",scout:"Allrounder",reviewer:"Review",manager:"Manager"},d=[{label:"Bilder",kw:["-vl-","-vl","vision","llava","multimodal","-mm-","pixtral"]},{label:"Coden",kw:["coder","-code-","code-","codestral","starcoder"]},{label:"Logik",kw:["-r1","deepseek-r1","reasoning","qwq","magistral","-think","thinking","-o1"]},{label:"Agenten & Tools",kw:["hermes","-tool","command-r","watt","-fc-","function"]}];function c(y){const k=(y||"").toLowerCase();for(const U of d)if(U.kw.some(F=>k.includes(F)))return U.label;return"Allrounder"}function u(y=""){const k=y.toLowerCase();return/\d+x\d+(\.\d+)?b/.test(k)||new RegExp("(?{mt(),Kt()});async function mt(){g(C,!0);try{const y=await fe("/api/cookbook/recipes");g(b,y.recipes||[],!0),g(A,y.recommended_id||null,!0)}catch(y){X(y.message,!0)}g(C,!1)}async function Kt(y=!1){g(B,!0);try{const k=await fe("/api/cookbook/discover"+(y?"?force=true":""));g(S,k,!0),g(ye,k.stale,!0)}catch(k){X("Empfehlungen nicht ladbar: "+k.message,!0)}g(B,!1)}function St(y){y=y.trim();const k=y.match(/huggingface\.co\/([^\s/]+\/[^\s/?#]+)/i);return k?k[1]:/^[\w.-]+\/[\w.-]+$/.test(y)?y:null}async function Et(){const y=n(ee).trim(),k=St(y);let U=y;if(n(I)&&!k&&(U=U?U+" "+n(I):n(I)),!U&&!k){g(ce,[],!0);return}g(_e,!0),g(ie,[],!0),g(ve,-1);try{if(k)g(ce,[{id:k,author:k.split("/")[0],downloads:0}],!0);else{const F=`https://huggingface.co/api/models?search=${encodeURIComponent(U)}&filter=gguf&sort=${encodeURIComponent(n(K))}&direction=-1&limit=40`,le=await fetch(F);g(ce,await le.json(),!0)}n(ce).forEach((F,le)=>P(le,F.id))}catch(F){X(F.message,!0),g(ce,[],!0)}g(_e,!1)}async function P(y,k){var U;try{const F=await fe("/api/cookbook/analyze",{method:"POST",body:JSON.stringify({repo_id:k,ctx:8192})});if(!((U=F.files)!=null&&U.length)){n(ie)[y]=null,N();return}const le=F.files.find(Qe=>{var Xe;return(Xe=Qe.quant)==null?void 0:Xe.includes("Q4_K_M")})||F.files[0];n(ie)[y]=le.fit.level,N()}catch{n(ie)[y]=null,N()}}function N(){let y=-1,k=99;n(ie).forEach((U,F)=>{if(U==null)return;const le=i[U]??99;le=0&&k<=1?y:-1,!0)}function J(y){g(me,n(b).find(k=>k.id===y),!0)}async function re(y){const k=document.getElementById("cb-r-install-btn");k&&(k.disabled=!0,k.textContent="Starte…");try{const U=await fe("/api/cookbook/install-recipe",{method:"POST",body:JSON.stringify({recipe_id:y,hf_token:Qa()})});X(`${U.count} Downloads gestartet — siehe Aktivität.`),g(me,null),_t("activity")}catch(U){X(U.message,!0),k&&(k.disabled=!1,k.textContent="Installieren")}}async function x(y,k){var U,F;g(Me,!0),g(Ue,k,!0),g(E,y,!0),g(Ae,""),g(Y,8192),g(Q,null),g(Z,null),g(de,""),g(be,!0);try{if(g(Q,await fe("/api/cookbook/analyze",{method:"POST",body:JSON.stringify({repo_id:y,ctx:8192})}),!0),(F=(U=n(Q))==null?void 0:U.files)!=null&&F.length){const le=n(Q).files.find(Qe=>{var Xe;return(Xe=Qe.quant)==null?void 0:Xe.includes("Q4_K_M")})||n(Q).files[0];g(de,le.filename,!0),le.optimal_ctx&&n(Y)===8192&&g(Y,le.optimal_ctx,!0),L()}}catch(le){X(le.message,!0)}g(be,!1)}function L(){var y,k,U;!((k=(y=n(Q))==null?void 0:y.files)!=null&&k.length)||!n(de)||(g(Z,n(Q).files.find(F=>F.filename===n(de))||null,!0),(U=n(Z))!=null&&U.optimal_ctx&&n(Y)===8192&&g(Y,n(Z).optimal_ctx,!0))}async function G(){if(!n(E)||!n(de))return X("Bitte eine GGUF-Datei wählen.",!0);g(be,!0);try{const y=await fe("/api/download",{method:"POST",body:JSON.stringify({repo:n(E),file:n(de),hf_token:Qa()})});await fe("/api/register",{method:"POST",body:JSON.stringify({role:n(Ae),model_path:y.expected_path,ctx:n(Y)})}),X("Download gestartet — siehe Aktivität."),g(Me,!1),_t("activity")}catch(y){X("Fehler: "+y.message,!0)}g(be,!1)}async function te(y,k,U,F){F.disabled=!0,F.textContent="Starte…";try{await fe("/api/cookbook/install-model",{method:"POST",body:JSON.stringify({repo:y,role:k,params_b:U,quant:"Q4_K_M",hf_token:Qa()})}),X("Download gestartet — siehe Aktivität."),_t("activity")}catch(le){X(le.message,!0),F.disabled=!1,F.textContent="Installieren"}}function $(){g(Re,null),g(Ce,""),g(He,""),g(Ie,[{repo:"",role:"",quant:"Q4_K_M"}],!0),g(ze,!0)}function H(y){const k=n(b).find(U=>U.id===y);k&&(g(Re,y,!0),g(Ce,k.title,!0),g(He,k.desc||"",!0),g(Ie,k.models.map(U=>({repo:U.repo,role:U.role,quant:U.quant||"Q4_K_M"})),!0),g(ze,!0))}async function ne(){if(!n(Ce).trim())return X("Bitte einen Titel angeben.",!0);const y=n(Ie).filter(k=>k.repo.trim());if(!y.length)return X("Mindestens ein Modell (Repo) angeben.",!0);try{n(Re)?(await fe("/api/cookbook/user-recipe/"+encodeURIComponent(n(Re)),{method:"PUT",body:JSON.stringify({title:n(Ce),desc:n(He),models:y})}),X("Setup aktualisiert.")):(await fe("/api/cookbook/user-recipe",{method:"POST",body:JSON.stringify({title:n(Ce),desc:n(He),models:y})}),X("Setup gespeichert.")),g(ze,!1),mt()}catch(k){X(k.message,!0)}}async function he(y){const k=n(b).find(U=>U.id===y);if(await bn({title:`„${(k==null?void 0:k.title)||y}" löschen?`,body:"Dein eigenes Setup wird aus dem Cookbook entfernt. Bereits installierte Modelle bleiben unangetastet.",confirmLabel:"Löschen",danger:!0}))try{await fe("/api/cookbook/user-recipe/"+encodeURIComponent(y),{method:"DELETE"}),X("Setup gelöscht."),mt()}catch(U){X(U.message,!0)}}function Ne(y){if(!u(y))return"";const k=f(y);return`MoE${k?` · ~${k}B aktiv`:""}`}function Se(y){return y?`${Cn(y)}`:""}var je=vc(),rt=l(ft(je),2),bt=s(rt),Ze=l(s(bt),2),p=l(bt,4);{var D=y=>{var k=qd();w(y,k)},se=y=>{var k=Kd();Ge(k,21,()=>n(b),Ke,(U,F)=>{const le=q(()=>n(F).id===n(A));var Qe=Fd(),Xe=s(Qe);{var Zt=Oe=>{var tt=Id(),Xt=ft(tt),ct=l(Xt,2);z("click",Xt,ut=>{ut.stopPropagation(),H(n(F).id)}),z("keydown",Xt,ut=>{ut.key==="Enter"&&(ut.stopPropagation(),H(n(F).id))}),z("click",ct,ut=>{ut.stopPropagation(),he(n(F).id)}),z("keydown",ct,ut=>{ut.key==="Enter"&&(ut.stopPropagation(),he(n(F).id))}),w(Oe,tt)};V(Xe,Oe=>{n(F).user&&Oe(Zt)})}var Ht=l(Xe,2);{var Vt=Oe=>{var tt=Ur();w(Oe,tt)},Wt=Oe=>{var tt=Nd();w(Oe,tt)};V(Ht,Oe=>{n(le)?Oe(Vt):n(F).user&&Oe(Wt,1)})}var Le=l(Ht,2),ke=s(Le),qe=s(ke),Fe=s(qe),De=l(ke,2),pt=s(De),kt=l(Le,2),Ct=s(kt),Lt=l(kt,2),et=s(Lt);O((Oe,tt)=>{lt(Qe,1,`card-btn${n(le)?" cb-best":""}`),m(Fe,n(F).title),lt(De,1,`fit-badge ${Oe??""}`),m(pt,tt),m(Ct,n(F).desc),m(et,`${n(F).models.length??""} Modell${n(F).models.length>1?"e":""} im Setup`)},[()=>_(n(F).fit_level),()=>T(n(F).fit_level)]),z("click",Qe,()=>J(n(F).id)),w(U,Qe)}),w(y,k)};V(p,y=>{n(C)?y(D):y(se,-1)})}var j=l(rt,2),xe=s(j),ge=l(s(xe),2),it=s(ge),Je=l(ge,2),dt=s(Je),Ve=l(xe,2),yt=s(Ve),Tt=l(Ve,2);{var Rt=y=>{var k=Gd();w(y,k)},Gt=y=>{var k=Lr(),U=ft(k);Ge(U,17,()=>n(S).categories,Ke,(F,le)=>{var Qe=jd(),Xe=s(Qe),Zt=s(Xe),Ht=s(Zt),Vt=l(Xe,2);Ge(Vt,21,()=>n(le).models,Ke,(Wt,Le)=>{const ke=q(()=>n(le).recommended&&n(Le).repo===n(le).recommended);var qe=Wd(),Fe=s(qe);{var De=Mt=>{var It=Ur();w(Mt,It)};V(Fe,Mt=>{n(ke)&&Mt(De)})}var pt=l(Fe,2),kt=s(pt),Ct=s(kt),Lt=s(Ct),et=l(Ct,2),Oe=s(et),tt=l(kt,2),Xt=s(tt),ct=l(pt,2),ut=s(ct);Ye(ut,()=>Se(o[n(Le).role]||c(n(Le).repo)));var Mn=l(ut,2);Ye(Mn,()=>Ne(n(Le).repo));var In=l(ct,2),We=s(In),nt=l(In,2);Ge(nt,17,()=>n(Le).requirements||[],Ke,(Mt,It)=>{var Nn=Ud(),Rr=s(Nn);O(()=>m(Rr,`⚠ ${n(It)??""}`)),w(Mt,Nn)});var st=l(nt,4),vt=s(st);{var en=Mt=>{var It=Hd();O(()=>zr(It,"title",`bereits eingerichtet als ${n(Le).installed??""}`)),w(Mt,It)},qt=Mt=>{var It=Vd();z("click",It,Nn=>te(n(Le).repo,n(Le).role,n(Le).params_b,Nn.currentTarget)),w(Mt,It)};V(vt,Mt=>{n(Le).installed?Mt(en):Mt(qt,-1)})}O((Mt,It,Nn)=>{lt(qe,1,`card${n(ke)?" res-best":""}`),m(Lt,n(Le).name),m(Oe,`${n(Le).author??""} · ${n(Le).params_b??""}B`),lt(tt,1,`fit-badge ${Mt??""}`),m(Xt,n(Le).fit.text),m(We,`${It??""} · ⬇ ${Nn??""}`)},[()=>_(n(Le).fit.level),()=>h(n(Le).fit),()=>(n(Le).downloads||0).toLocaleString()]),w(Wt,qe)}),O(()=>m(Ht,n(le).title)),w(F,Qe)}),w(y,k)};V(Tt,y=>{var k;n(B)&&!n(S)?y(Rt):(k=n(S))!=null&&k.categories&&y(Gt,1)})}var wt=l(j,2),Ut=l(s(wt),4),Ee=s(Ut),Be=l(Ee,2),$t=s(Be);$t.value=$t.__value="downloads";var sn=l($t);sn.value=sn.__value="trending";var ln=l(sn);ln.value=ln.__value="created_at";var xt=l(Be,2),kn=s(xt),Pe=l(Ut,2);Ge(Pe,20,()=>["","coder","vision","reasoning","chat","agent"],Ke,(y,k)=>{var U=Jd(),F=s(U);O(()=>{lt(U,1,n(I)===k?"primary":"ghost"),m(F,k||"Alle")}),z("click",U,()=>{g(I,k,!0),Et()}),w(y,U)});var At=l(Pe,2);{var Or=y=>{var k=Yd();Ge(k,21,()=>n(ce),Ke,(U,F,le)=>{var Qe=Qd(),Xe=s(Qe);{var Zt=ct=>{var ut=Ur();w(ct,ut)};V(Xe,ct=>{le===n(ve)&&ct(Zt)})}var Ht=l(Xe,2),Vt=s(Ht),Wt=s(Vt),Le=s(Wt),ke=l(Wt,2),qe=s(ke),Fe=l(Vt,2),De=s(Fe),pt=l(Ht,2),kt=s(pt);Ye(kt,()=>Se(c(n(F).id)));var Ct=l(kt,2);Ye(Ct,()=>Ne(n(F).id));var Lt=l(pt,4),et=s(Lt),Oe=s(et),tt=l(et,2),Xt=s(tt);O((ct,ut,Mn,In,We)=>{lt(Qe,1,`card${le===n(ve)?" res-best":""}`),m(Le,ct),m(qe,n(F).author||""),lt(Fe,1,`fit-badge ${ut??""}`),m(De,Mn),m(Oe,In),m(Xt,`⬇ ${We??""}`)},[()=>n(F).id.split("/").pop(),()=>n(ie)[le]?_(n(ie)[le]):"warn",()=>n(ie)[le]?T(n(ie)[le]):"prüfe…",()=>n(ie)[le]?h({req_gb:0,tps:0}):"Hardware-Fit…",()=>(n(F).downloads||0).toLocaleString()]),z("click",Qe,()=>x(n(F).id,n(F).id.split("/").pop())),w(U,Qe)}),w(y,k)};V(At,y=>{n(ce).length&&y(Or)})}var ki=l(wt,2);{var Ys=y=>{const k=q(()=>n(me));var U=ac(),F=s(U),le=s(F),Qe=l(le,2),Xe=s(Qe),Zt=l(Qe,2),Ht=s(Zt),Vt=l(Zt,2);Ge(Vt,21,()=>n(k).models,Ke,(Fe,De)=>{var pt=tc(),kt=s(pt),Ct=s(kt),Lt=s(Ct),et=s(Lt),Oe=l(Lt,2),tt=s(Oe),Xt=l(Oe,2);Ye(Xt,()=>Ne(n(De).repo));var ct=l(Ct,2);{var ut=st=>{var vt=Zd(),en=s(vt);O(()=>m(en,n(De).why)),w(st,vt)};V(ct,st=>{n(De).why&&st(ut)})}var Mn=l(ct,2);{var In=st=>{var vt=Xd(),en=s(vt);O((qt,Mt,It)=>m(en,`~${qt??""} GB · ~${Mt??""} Tok/s · optimal ~${It??""}k Kontext`),[()=>(n(De).fit.req_gb??0).toFixed(1),()=>Math.round(n(De).fit.tps??0),()=>Math.round((n(De).optimal_ctx||8192)/1024)]),w(st,vt)};V(Mn,st=>{n(De).fit&&st(In)})}var We=l(kt,2);{var nt=st=>{var vt=ec(),en=s(vt);O(qt=>{lt(vt,1,`fit-badge ${qt??""}`),m(en,n(De).fit.text)},[()=>_(n(De).fit.level)]),w(st,vt)};V(We,st=>{n(De).fit&&st(nt)})}O(()=>{m(et,n(De).name),m(tt,n(De).role)}),w(Fe,pt)});var Wt=l(Vt,2);{var Le=Fe=>{const De=q(()=>Math.max(...n(k).models.map(Lt=>{var et;return((et=Lt.fit)==null?void 0:et.req_gb)??0})));var pt=nc(),kt=l(s(pt)),Ct=s(kt);O(Lt=>m(Ct,`~${Lt??""} GB`),[()=>n(De).toFixed(1)]),w(Fe,pt)};V(Wt,Fe=>{n(k).models.length&&n(k).models[0].fit&&Fe(Le)})}var ke=l(Wt,2),qe=s(ke);O(()=>{m(Xe,n(k).title),m(Ht,n(k).desc),lt(ke,1,`primary${n(k).fit_level==="too_tight"?" warn":""}`),m(qe,n(k).fit_level==="too_tight"?"Trotzdem installieren (zu groß)":"Komplettes Setup installieren")}),z("click",U,Fe=>{Fe.target.classList.contains("modal-overlay")&&g(me,null)}),z("click",le,()=>g(me,null)),z("click",ke,()=>re(n(k).id)),w(y,U)};V(ki,y=>{n(me)&&y(Ys)})}var Mi=l(ki,2);{var Zs=y=>{var k=dc(),U=s(k),F=s(U),le=l(F,2),Qe=s(le),Xe=l(le,2),Zt=s(Xe),Ht=l(Xe,2);{var Vt=ke=>{var qe=rc();w(ke,qe)},Wt=ke=>{var qe=lc(),Fe=l(ft(qe),2);Ge(Fe,21,()=>n(Q).files,Ke,(We,nt)=>{const st=q(()=>n(nt).fit.level==="perfect"?"●":n(nt).fit.level==="marginal"?"◐":"○");var vt=Hi(),en=s(vt),qt={};O(()=>{m(en,`${n(st)??""} ${n(nt).filename??""}`),qt!==(qt=n(nt).filename)&&(vt.value=(vt.__value=n(nt).filename)??"")}),w(We,vt)});var De=l(Fe,2),pt=s(De),kt=l(s(pt),2),Ct=l(pt,2),Lt=s(Ct),et=l(s(Lt));Ye(et,()=>li(a));var Oe=l(Lt,2),tt=l(De,2);{var Xt=We=>{var nt=ic(),st=l(s(nt)),vt=s(st),en=l(st,2);O(qt=>m(vt,`~${qt??""}k`),[()=>Math.round(n(Z).optimal_ctx/1024)]),z("click",en,qt=>{qt.preventDefault(),g(Y,n(Z).optimal_ctx,!0)}),w(We,nt)};V(tt,We=>{var nt;(nt=n(Z))!=null&&nt.optimal_ctx&&We(Xt)})}var ct=l(tt,2);{var ut=We=>{var nt=sc(),st=s(nt),vt=l(s(st),2),en=s(vt),qt=l(st,2),Mt=s(qt);O((It,Nn,Rr)=>{m(en,`~${It??""} GB · ~${Nn??""} Tok/s · ${(n(Z).quant||"GGUF")??""}`),lt(qt,1,`fit-badge ${Rr??""}`),m(Mt,n(Z).fit.text)},[()=>(n(Z).fit.req_gb??0).toFixed(1),()=>Math.round(n(Z).fit.tps??0),()=>_(n(Z).fit.level)]),w(We,nt)};V(ct,We=>{n(Z)&&We(ut)})}var Mn=l(ct,2),In=s(Mn);O(()=>{var We,nt;lt(Mn,1,`primary${((We=n(Z))==null?void 0:We.fit.level)==="too_tight"?" warn":""}`),Mn.disabled=n(be),m(In,n(be)?"Starte…":((nt=n(Z))==null?void 0:nt.fit.level)==="too_tight"?"Trotzdem holen (zu groß)":"Herunterladen & Einpflegen")}),z("change",Fe,L),ja(Fe,()=>n(de),We=>g(de,We)),Sn(kt,()=>n(Ae),We=>g(Ae,We)),Sn(Oe,()=>n(Y),We=>g(Y,We)),z("click",Mn,G),w(ke,qe)},Le=ke=>{var qe=oc();w(ke,qe)};V(Ht,ke=>{var qe,Fe;n(be)&&!n(Q)?ke(Vt):(Fe=(qe=n(Q))==null?void 0:qe.files)!=null&&Fe.length?ke(Wt,1):n(be)||ke(Le,2)})}O(()=>{m(Qe,n(Ue)),m(Zt,n(E))}),z("click",k,ke=>{ke.target.classList.contains("modal-overlay")&&g(Me,!1)}),z("click",F,()=>g(Me,!1)),w(y,k)};V(Mi,y=>{n(Me)&&y(Zs)})}var Xs=l(Mi,2);{var el=y=>{var k=uc(),U=s(k),F=s(U),le=l(F,2),Qe=s(le),Xe=l(le,6),Zt=l(Xe,4),Ht=l(Zt,4);Ge(Ht,17,()=>n(Ie),Ke,(ke,qe,Fe)=>{var De=cc(),pt=s(De),kt=l(pt,2),Ct=l(kt,2);Ge(Ct,20,()=>["Q4_K_M","Q5_K_M","Q6_K","Q8_0","Q3_K_M"],Ke,(et,Oe)=>{var tt=Hi(),Xt=s(tt),ct={};O(()=>{m(Xt,Oe),ct!==(ct=Oe)&&(tt.__value=Oe)}),w(et,tt)});var Lt=l(Ct,2);Sn(pt,()=>n(qe).repo,et=>n(qe).repo=et),Sn(kt,()=>n(qe).role,et=>n(qe).role=et),ja(Ct,()=>n(qe).quant,et=>n(qe).quant=et),z("click",Lt,()=>g(Ie,n(Ie).filter((et,Oe)=>Oe!==Fe),!0)),w(ke,De)});var Vt=l(Ht,2),Wt=l(Vt,4),Le=s(Wt);O(()=>{m(Qe,n(Re)?"Setup bearbeiten":"Eigenes Setup erstellen"),m(Le,n(Re)?"Änderungen speichern":"Setup speichern")}),z("click",k,ke=>{ke.target.classList.contains("modal-overlay")&&g(ze,!1)}),z("click",F,()=>g(ze,!1)),Sn(Xe,()=>n(Ce),ke=>g(Ce,ke)),Sn(Zt,()=>n(He),ke=>g(He,ke)),z("click",Vt,()=>g(Ie,[...n(Ie),{repo:"",role:"",quant:"Q4_K_M"}],!0)),z("click",Wt,ne),w(y,k)};V(Xs,y=>{n(ze)&&y(el)})}O(()=>{m(it,n(S)?"aktuell":""),Je.disabled=n(B),m(dt,n(B)?"Suche…":"Aktualisieren"),m(yt,`Täglich aktualisierte Top-Modelle je Kategorie — Hardware-Fit berechnet für dein System. ${n(ye)?"⚠ Cache veraltet.":""}`),xt.disabled=n(_e),m(kn,n(_e)?"Lade…":"Suchen")}),z("click",Ze,$),z("click",Je,()=>Kt(!0)),z("keydown",Ee,y=>y.key==="Enter"&&Et()),Sn(Ee,()=>n(ee),y=>g(ee,y)),ja(Be,()=>n(K),y=>g(K,y)),z("click",xt,Et),w(e,je),Zn()}_a(["click","keydown","change"]);var pc=M(`
Du öffnest Mission Control über einen Proxy/Namen. Die Engine läuft auf Port 8080 ohne HTTPS direkt am Bosgame. + Trag in deinen Tools die LAN-IP ein: http://192.168.178.151:8080/v1 — nicht den Proxy-Namen.
`),hc=M(' '),gc=M('
'),_c=M(' '),mc=M(''),bc=M('

2 · Modelle auswählen

Wähle, welche Modelle im Config-Snippet erscheinen sollen.
'),yc=M(''),wc=M('

Öffne die Zed-Settings mit Cmd/Strg + , und füge diesen Block in deine bestehende settings.json ein. Falls bereits ein "bosgame"-Eintrag existiert: vorher löschen.

Nur diesen Block einfügen, nicht die ganze Datei ersetzen — sonst gehen andere Einstellungen verloren.

API-Key: Öffne den Agent-Panel → Anbieter bosgame → trag irgendeinen Wert ein (z.B. local).

Danach im Agent-Panel das Modell auswählen.
',1),xc=M('

Datei anlegen oder öffnen:

Windows: %USERPROFILE%\\.config\\opencode\\opencode.json

Nur den "provider"-Block einfügen. Falls bereits ein "llama-swap"-Eintrag vorhanden: vorher löschen, sonst erscheinen Modelle doppelt.

Danach in OpenCode /models wählen.

',1),kc=M('

In Cline/Cursor den Provider „OpenAI Compatible" wählen:

API-Key: ein beliebiger Wert (z.B. local).
',1),Mc=M('

Settings → Admin Panel → Connections → neue OpenAI-Verbindung:

',1),Sc=M('

Bilder einbinden (Vision)

Mit Vision-Modellen kannst du Screenshots oder Fehlerbilder direkt mitschicken.
'),Ec=M('

Verbinden

Schritt für Schritt: KI-Modelle in deine Tools einbinden — getestet, erklärt, mit Copy-Paste-Config.

1 · Verbindung prüfen

Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.

Wie funktioniert das Auto-Swap?

3 · Tool wählen

Für welches Werkzeug brauchst du die Konfiguration?

4 · Config-Snippet

',1);function Tc(e,t){Yn(t,!0);const a=q(()=>dr.value),r=q(()=>{var P;return(((P=n(a))==null?void 0:P.models)||[]).filter(N=>!N.incomplete)}),i=q(f);let o=R("zed"),d=R(Bt([])),c=R(""),u=R(!1);function f(){return`http://${location.hostname}:8080/v1`}function _(P){return/^\d{1,3}(\.\d{1,3}){3}$/.test(P)}function T(){return location.protocol==="https:"||!_(location.hostname)}const h=q(()=>n(d).length?n(d):n(r).map(P=>P.name)),b=q(()=>n(h)[0]||"coder"),A=q(()=>{var P;return(P=n(r).find(N=>{var J,re;return(re=(J=N.meta)==null?void 0:J.caps)==null?void 0:re.includes("Bild")}))==null?void 0:P.name});function C(P,N){const re=(N.length?n(r).filter(x=>N.includes(x.name)):n(r).length?n(r):[{name:"coder",meta:{}}]).map(x=>{var te,$;const L=(((te=x.meta)==null?void 0:te.caps)||[]).some(H=>/Bild|Vision/i.test(H)),G=(($=x.meta)==null?void 0:$.ctx)||131072;return` { "name": "${x.name}", "display_name": "${x.name}", "max_tokens": ${G}${L?', "capabilities": { "tools": true, "images": true }':""} }`}).join(`, `);return`"language_models": { "openai_compatible": { "bosgame": { - "api_url": "${L}", + "api_url": "${P}", "available_models": [ -${A} +${re} ] } } -}`}function E(L,K){const A=(K.length?K:n(r).length?n(r).map(q=>q.name):["coder"]).map(q=>` "${q}": { "name": "${q}" }`).join(`, +}`}function S(P,N){const re=(N.length?N:n(r).length?n(r).map(x=>x.name):["coder"]).map(x=>` "${x}": { "name": "${x}" }`).join(`, `);return`"provider": { "llama-swap": { "npm": "@ai-sdk/openai-compatible", "name": "Bosgame (llama-swap)", "options": { - "baseURL": "${L}", + "baseURL": "${P}", "apiKey": "local" }, "models": { -${A} +${re} } } -}`}async function I(){_(c,"Teste…");try{const L=await fe("/api/integration/test");_(u,L.ok,!0),_(c,L.ok?`✓ Verbunden — ${L.models.length} Modell(e): ${L.models.join(", ")}`:`✗ Keine Verbindung: ${L.error}`,!0)}catch(L){_(u,!1),_(c,"✗ "+L.message)}}function de(L){var w;const K=L.querySelector("code");K&&((w=navigator.clipboard)==null||w.writeText(K.textContent||""))}function Y(L){var K;(K=navigator.clipboard)==null||K.writeText(L.value)}var ce=Sc(),me=l(ft(ce),2),U=l(s(me),6),F=s(U),ie=l(F,2),he=l(U,2);{var _e=L=>{var K=fc();x(L,K)},Me=N(()=>$());V(he,L=>{n(Me)&&L(_e)})}var He=l(he,2),T=s(He),J=l(T,2);{var se=L=>{var K=pc(),w=s(K);z(()=>{jn(K,`color:${n(u)?"#7ee29a":"#ff9b95"}`),m(w,n(c))}),x(L,K)};V(J,L=>{n(c)&&L(se)})}var $e=l(me,2),Q=l(s($e),2);Ue(Q,20,()=>[["⚡","Kein manuelles Starten nötig",'Wenn du in Zed oder OpenCode „coder" auswählst und eine Anfrage sendest, lädt llama-swap das Modell automatisch — in ca. 2–5 Sekunden.'],["🔄","Modelle wechseln sich automatisch ab","Rufst du ein anderes Modell auf, entlädt llama-swap das aktuelle (nach TTL-Ablauf) und lädt das neue. Es läuft immer nur eines gleichzeitig."],["🧠","Zwei Modelle gleichzeitig aktiv halten","Du hast 124 GB RAM — genug für z.B. Scout 8B (~5 GB) dauerhaft geladen + ein größeres Modell das swappt. Dafür beim Scout im Modelle-Tab → Konfigurieren die TTL auf 99999 setzen."]],Ge,(L,K)=>{var w=N(()=>Qa(K,3));let A=()=>n(w)[0],q=()=>n(w)[1],ee=()=>n(w)[2];var O=hc(),ue=s(O),B=s(ue),j=l(ue,2),te=s(j),ae=s(te),De=l(te,2),Ae=s(De);z(()=>{m(B,A()),m(ae,q()),m(Ae,ee())}),x(L,O)});var be=l($e,2);{var X=L=>{var K=mc(),w=s(K),A=l(s(w),2),q=s(A),ee=l(w,4);Ue(ee,21,()=>n(r),Ge,(B,j)=>{const te=N(()=>n(d).includes(n(j).name));var ae=gc(),De=s(ae);z(()=>{jn(ae,`cursor:pointer;${n(te)?"background:rgba(45,212,191,.2);border-color:var(--accent);color:var(--accent)":""}`),m(De,n(j).name)}),C("click",ae,()=>_(d,n(te)?n(d).filter(Ae=>Ae!==n(j).name):[...n(d),n(j).name],!0)),C("keydown",ae,Ae=>Ae.key==="Enter"&&_(d,n(te)?n(d).filter(Ce=>Ce!==n(j).name):[...n(d),n(j).name],!0)),x(B,ae)});var O=l(ee,2);{var ue=B=>{var j=_c(),te=s(j);C("click",te,ae=>{ae.preventDefault(),_(d,[],!0)}),x(B,j)};V(O,B=>{n(d).length&&B(ue)})}z(()=>m(q,n(d).length?`${n(d).length} von ${n(r).length} ausgewählt`:"alle werden eingeschlossen")),x(L,K)};V(be,L=>{n(r).length&&L(X)})}var ze=l(be,2),Se=l(s(ze),4);Ue(Se,20,()=>[["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"]],Ge,(L,K)=>{var w=N(()=>Qa(K,3));let A=()=>n(w)[0],q=()=>n(w)[1],ee=()=>n(w)[2];var O=bc(),ue=s(O),B=s(ue),j=l(ue,2),te=s(j);z(()=>{st(O,1,`card-btn${n(o)===A()?" cb-best":""}`),m(B,q()),m(te,ee())}),C("click",O,()=>_(o,A(),!0)),x(L,O)});var Ie=l(ze,2),Ve=l(s(Ie),2);{var Fe=L=>{const K=N(()=>P(n(i),n(h)));var w=wc(),A=l(ft(w),4),q=s(A),ee=l(q,2),O=s(ee),ue=s(O),B=l(A,4),j=l(s(B)),te=s(j);z(()=>{m(ue,n(K)),m(te,`bosgame / ${n(b)??""}`)}),C("click",q,ae=>de(ae.currentTarget.parentElement)),x(L,w)},mt=L=>{const K=N(()=>E(n(i),n(h)));var w=yc(),A=l(ft(w),6),q=s(A),ee=l(q,2),O=s(ee),ue=s(O),B=l(A,2),j=l(s(B),3),te=s(j);z(()=>{m(ue,n(K)),m(te,`llama-swap/${n(b)??""}`)}),C("click",q,ae=>de(ae.currentTarget.parentElement)),x(L,w)},bt=L=>{var K=xc(),w=l(ft(K),4),A=s(w),q=l(A,2),ee=l(w,4),O=s(ee),ue=l(O,2);z(()=>{ur(A,n(i)),ur(O,n(b))}),C("click",q,B=>Y(B.currentTarget.previousElementSibling)),C("click",ue,B=>Y(B.currentTarget.previousElementSibling)),x(L,K)},Kt=L=>{var K=kc(),w=l(ft(K),4),A=s(w),q=l(A,2),ee=l(w,4),O=s(ee),ue=l(O,2);z(()=>ur(A,n(i))),C("click",q,B=>Y(B.currentTarget.previousElementSibling)),C("click",ue,B=>Y(B.currentTarget.previousElementSibling)),x(L,K)};V(Ve,L=>{n(o)==="zed"?L(Fe):n(o)==="opencode"?L(mt,1):n(o)==="cline"?L(bt,2):n(o)==="openwebui"&&L(Kt,3)})}var wt=l(Ie,2);{var Mt=L=>{const K=N(()=>`curl ${n(i)}/chat/completions \\ +}`}async function B(){g(c,"Teste…");try{const P=await fe("/api/integration/test");g(u,P.ok,!0),g(c,P.ok?`✓ Verbunden — ${P.models.length} Modell(e): ${P.models.join(", ")}`:`✗ Keine Verbindung: ${P.error}`,!0)}catch(P){g(u,!1),g(c,"✗ "+P.message)}}function ye(P){var J;const N=P.querySelector("code");N&&((J=navigator.clipboard)==null||J.writeText(N.textContent||""))}function ee(P){var N;(N=navigator.clipboard)==null||N.writeText(P.value)}var ce=Ec(),_e=l(ft(ce),2),I=l(s(_e),6),K=s(I),ie=l(K,2),ve=l(I,2);{var me=P=>{var N=pc();w(P,N)},Me=q(()=>T());V(ve,P=>{n(Me)&&P(me)})}var Ue=l(ve,2),E=s(Ue),Q=l(E,2);{var de=P=>{var N=hc(),J=s(N);O(()=>{Bn(N,`color:${n(u)?"#7ee29a":"#ff9b95"}`),m(J,n(c))}),w(P,N)};V(Q,P=>{n(c)&&P(de)})}var Ae=l(_e,2),Y=l(s(Ae),2);Ge(Y,20,()=>[["⚡","Kein manuelles Starten nötig",'Wenn du in Zed oder OpenCode „coder" auswählst und eine Anfrage sendest, lädt llama-swap das Modell automatisch — in ca. 2–5 Sekunden.'],["🔄","Modelle wechseln sich automatisch ab","Rufst du ein anderes Modell auf, entlädt llama-swap das aktuelle (nach TTL-Ablauf) und lädt das neue. Es läuft immer nur eines gleichzeitig."],["🧠","Zwei Modelle gleichzeitig aktiv halten","Du hast 124 GB RAM — genug für z.B. Scout 8B (~5 GB) dauerhaft geladen + ein größeres Modell das swappt. Dafür beim Scout im Modelle-Tab → Konfigurieren die TTL auf 99999 setzen."]],Ke,(P,N)=>{var J=q(()=>Ya(N,3));let re=()=>n(J)[0],x=()=>n(J)[1],L=()=>n(J)[2];var G=gc(),te=s(G),$=s(te),H=l(te,2),ne=s(H),he=s(ne),Ne=l(ne,2),Se=s(Ne);O(()=>{m($,re()),m(he,x()),m(Se,L())}),w(P,G)});var be=l(Ae,2);{var Z=P=>{var N=bc(),J=s(N),re=l(s(J),2),x=s(re),L=l(J,4);Ge(L,21,()=>n(r),Ke,($,H)=>{const ne=q(()=>n(d).includes(n(H).name));var he=_c(),Ne=s(he);O(()=>{Bn(he,`cursor:pointer;${n(ne)?"background:rgba(45,212,191,.2);border-color:var(--accent);color:var(--accent)":""}`),m(Ne,n(H).name)}),z("click",he,()=>g(d,n(ne)?n(d).filter(Se=>Se!==n(H).name):[...n(d),n(H).name],!0)),z("keydown",he,Se=>Se.key==="Enter"&&g(d,n(ne)?n(d).filter(je=>je!==n(H).name):[...n(d),n(H).name],!0)),w($,he)});var G=l(L,2);{var te=$=>{var H=mc(),ne=s(H);z("click",ne,he=>{he.preventDefault(),g(d,[],!0)}),w($,H)};V(G,$=>{n(d).length&&$(te)})}O(()=>m(x,n(d).length?`${n(d).length} von ${n(r).length} ausgewählt`:"alle werden eingeschlossen")),w(P,N)};V(be,P=>{n(r).length&&P(Z)})}var ze=l(be,2),Ce=l(s(ze),4);Ge(Ce,20,()=>[["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"]],Ke,(P,N)=>{var J=q(()=>Ya(N,3));let re=()=>n(J)[0],x=()=>n(J)[1],L=()=>n(J)[2];var G=yc(),te=s(G),$=s(te),H=l(te,2),ne=s(H);O(()=>{lt(G,1,`card-btn${n(o)===re()?" cb-best":""}`),m($,x()),m(ne,L())}),z("click",G,()=>g(o,re(),!0)),w(P,G)});var He=l(ze,2),Ie=l(s(He),2);{var Re=P=>{const N=q(()=>C(n(i),n(h)));var J=wc(),re=l(ft(J),4),x=s(re),L=l(x,2),G=s(L),te=s(G),$=l(re,4),H=l(s($)),ne=s(H);O(()=>{m(te,n(N)),m(ne,`bosgame / ${n(b)??""}`)}),z("click",x,he=>ye(he.currentTarget.parentElement)),w(P,J)},_t=P=>{const N=q(()=>S(n(i),n(h)));var J=xc(),re=l(ft(J),6),x=s(re),L=l(x,2),G=s(L),te=s(G),$=l(re,2),H=l(s($),3),ne=s(H);O(()=>{m(te,n(N)),m(ne,`llama-swap/${n(b)??""}`)}),z("click",x,he=>ye(he.currentTarget.parentElement)),w(P,J)},mt=P=>{var N=kc(),J=l(ft(N),4),re=s(J),x=l(re,2),L=l(J,4),G=s(L),te=l(G,2);O(()=>{ur(re,n(i)),ur(G,n(b))}),z("click",x,$=>ee($.currentTarget.previousElementSibling)),z("click",te,$=>ee($.currentTarget.previousElementSibling)),w(P,N)},Kt=P=>{var N=Mc(),J=l(ft(N),4),re=s(J),x=l(re,2),L=l(J,4),G=s(L),te=l(G,2);O(()=>ur(re,n(i))),z("click",x,$=>ee($.currentTarget.previousElementSibling)),z("click",te,$=>ee($.currentTarget.previousElementSibling)),w(P,N)};V(Ie,P=>{n(o)==="zed"?P(Re):n(o)==="opencode"?P(_t,1):n(o)==="cline"?P(mt,2):n(o)==="openwebui"&&P(Kt,3)})}var St=l(He,2);{var Et=P=>{const N=q(()=>`curl ${n(i)}/chat/completions \\ -H "Content-Type: application/json" \\ -d '{ - "model": "${n(S)}", + "model": "${n(A)}", "messages": [{"role":"user","content":[{"type":"text","text":"Was ist auf dem Bild falsch?"},{"type":"image_url","image_url":{"url":"data:image/png;base64,"}}]}] -}'`);var w=Mc(),A=s(w),q=l(s(A),2),ee=s(q);Qe(ee,()=>ea("check"));var O=l(ee),ue=l(A,4),B=s(ue),j=l(B,2),te=s(j),ae=s(te);z(()=>{m(O,` „${n(S)??""}" bereit`),m(ae,n(K))}),C("click",B,De=>de(De.currentTarget.parentElement)),x(L,w)};V(wt,L=>{n(S)&&L(Mt)})}z(()=>ur(F,n(i))),C("click",ie,L=>{const K=L.currentTarget.previousElementSibling;Y(K)}),C("click",T,I),x(e,ce),Zn()}_a(["click","keydown"]);var Tc=M('
'),$c=M('

Für dich: Upgrades

Neuere Modelle, die ein installiertes ersetzen können.
'),Ac=M('
Lade News… (erstes Laden kann kurz dauern)
'),Cc=M('
'),Lc=M('
Keine News geladen
Die Quellen sind gerade nicht erreichbar — später nochmal probieren.
'),Pc=M('für dich'),zc=M('
'),Dc=M('
'),Oc=M('

News

Aktuelles aus der Local-LLM-Welt — aus kuratierten Quellen.
',1);function Rc(e,t){Yn(t,!0);let a=R(Bt([])),r=R(Bt([])),i=R(!0),o=R("");const d=U=>U==="perfect"?"ok":U==="marginal"?"warn":"bad";function c(U){const F=Math.max(0,Date.now()/1e3-U);return F<60?F+"s":F<3600?Math.floor(F/60)+"m":F<86400?Math.floor(F/3600)+"h":Math.floor(F/86400)+"d"}async function u(){_(i,!0),_(o,"");try{_(a,(await fe("/api/news")).items||[],!0)}catch(U){_(o,U.message||"Fehler beim Laden",!0)}finally{_(i,!1)}}async function f(){try{_(r,(await fe("/api/cookbook/upgrades")).upgrades||[],!0)}catch{_(r,[],!0)}}async function g(U,F){var ie;F.disabled=!0,F.textContent="Starte…";try{await fe("/api/cookbook/install-model",{method:"POST",body:JSON.stringify({repo:U.repo,role:U.role,params_b:U.params_b,quant:U.quant,hf_token:Ja()})}),(ie=document.querySelector(".nav-item[data-view='activity']"))==null||ie.dispatchEvent(new MouseEvent("click"))}catch(he){F.disabled=!1,F.textContent="Installieren",alert("Fehler: "+he.message)}}function $(){u(),f()}or(()=>{u(),f()});var h=Oc(),b=ft(h),S=l(s(b),2),P=l(b,2);{var E=U=>{var F=$c(),ie=s(F),he=l(s(ie),2),_e=s(he),Me=l(ie,4);Ue(Me,21,()=>n(r),Ge,(He,T)=>{var J=Tc(),se=s(J),$e=s(se),Q=s($e),be=s(Q),X=l(Q,2),ze=s(X),Se=l($e,2),Ie=s(Se),Ve=l(se,2);z(Fe=>{m(be,n(T).name),st(X,1,`fit-badge ${Fe??""}`),m(ze,n(T).fit.text),m(Ie,`Ersetzt dein „${n(T).old??""}". ${n(T).why??""}`)},[()=>d(n(T).fit.level)]),C("click",Ve,Fe=>g(n(T),Fe.currentTarget)),x(He,J)}),z(()=>m(_e,n(r).length)),x(U,F)};V(P,U=>{n(r).length&&U(E)})}var I=l(P,2);{var de=U=>{var F=Ac();x(U,F)},Y=U=>{var F=Cc(),ie=s(F);z(()=>m(ie,n(o))),x(U,F)},ce=U=>{var F=Lc();x(U,F)},me=U=>{var F=Dc();Ue(F,21,()=>n(a),Ge,(ie,he,_e)=>{var Me=zc();st(Me,1,`news-card${_e===0?" featured":""}`);var He=s(Me),T=s(He),J=s(T),se=l(T,2);{var $e=Se=>{var Ie=Pc();x(Se,Ie)};V(se,Se=>{n(he).relevant&&Se($e)})}var Q=l(se,2),be=s(Q),X=l(He,2),ze=s(X);z(Se=>{Lr(Me,"href",n(he).link),m(J,n(he).source),m(be,Se),m(ze,n(he).title)},[()=>n(he).ts?c(n(he).ts):""]),x(ie,Me)}),x(U,F)};V(I,U=>{n(i)?U(de):n(o)?U(Y,1):n(a).length?U(me,-1):U(ce,2)})}C("click",S,$),x(e,h),Zn()}_a(["click"]);const Bc=[["Dein erster Workflow — in 3 Schritten",` +}'`);var J=Sc(),re=s(J),x=l(s(re),2),L=s(x);Ye(L,()=>ea("check"));var G=l(L),te=l(re,4),$=s(te),H=l($,2),ne=s(H),he=s(ne);O(()=>{m(G,` „${n(A)??""}" bereit`),m(he,n(N))}),z("click",$,Ne=>ye(Ne.currentTarget.parentElement)),w(P,J)};V(St,P=>{n(A)&&P(Et)})}O(()=>ur(K,n(i))),z("click",ie,P=>{const N=P.currentTarget.previousElementSibling;ee(N)}),z("click",E,B),w(e,ce),Zn()}_a(["click","keydown"]);var $c=M('
'),Ac=M('

Für dich: Upgrades

Neuere Modelle, die ein installiertes ersetzen können.
'),Cc=M('
Lade News… (erstes Laden kann kurz dauern)
'),Lc=M('
'),zc=M('
Keine News geladen
Die Quellen sind gerade nicht erreichbar — später nochmal probieren.
'),Pc=M('für dich'),Dc=M('
'),Oc=M('
'),Rc=M('

News

Aktuelles aus der Local-LLM-Welt — aus kuratierten Quellen.
',1);function Bc(e,t){Yn(t,!0);let a=R(Bt([])),r=R(Bt([])),i=R(!0),o=R("");const d=I=>I==="perfect"?"ok":I==="marginal"?"warn":"bad";function c(I){const K=Math.max(0,Date.now()/1e3-I);return K<60?K+"s":K<3600?Math.floor(K/60)+"m":K<86400?Math.floor(K/3600)+"h":Math.floor(K/86400)+"d"}async function u(){g(i,!0),g(o,"");try{g(a,(await fe("/api/news")).items||[],!0)}catch(I){g(o,I.message||"Fehler beim Laden",!0)}finally{g(i,!1)}}async function f(){try{g(r,(await fe("/api/cookbook/upgrades")).upgrades||[],!0)}catch{g(r,[],!0)}}async function _(I,K){var ie;K.disabled=!0,K.textContent="Starte…";try{await fe("/api/cookbook/install-model",{method:"POST",body:JSON.stringify({repo:I.repo,role:I.role,params_b:I.params_b,quant:I.quant,hf_token:Qa()})}),(ie=document.querySelector(".nav-item[data-view='activity']"))==null||ie.dispatchEvent(new MouseEvent("click"))}catch(ve){K.disabled=!1,K.textContent="Installieren",alert("Fehler: "+ve.message)}}function T(){u(),f()}or(()=>{u(),f()});var h=Rc(),b=ft(h),A=l(s(b),2),C=l(b,2);{var S=I=>{var K=Ac(),ie=s(K),ve=l(s(ie),2),me=s(ve),Me=l(ie,4);Ge(Me,21,()=>n(r),Ke,(Ue,E)=>{var Q=$c(),de=s(Q),Ae=s(de),Y=s(Ae),be=s(Y),Z=l(Y,2),ze=s(Z),Ce=l(Ae,2),He=s(Ce),Ie=l(de,2);O(Re=>{m(be,n(E).name),lt(Z,1,`fit-badge ${Re??""}`),m(ze,n(E).fit.text),m(He,`Ersetzt dein „${n(E).old??""}". ${n(E).why??""}`)},[()=>d(n(E).fit.level)]),z("click",Ie,Re=>_(n(E),Re.currentTarget)),w(Ue,Q)}),O(()=>m(me,n(r).length)),w(I,K)};V(C,I=>{n(r).length&&I(S)})}var B=l(C,2);{var ye=I=>{var K=Cc();w(I,K)},ee=I=>{var K=Lc(),ie=s(K);O(()=>m(ie,n(o))),w(I,K)},ce=I=>{var K=zc();w(I,K)},_e=I=>{var K=Oc();Ge(K,21,()=>n(a),Ke,(ie,ve,me)=>{var Me=Dc();lt(Me,1,`news-card${me===0?" featured":""}`);var Ue=s(Me),E=s(Ue),Q=s(E),de=l(E,2);{var Ae=Ce=>{var He=Pc();w(Ce,He)};V(de,Ce=>{n(ve).relevant&&Ce(Ae)})}var Y=l(de,2),be=s(Y),Z=l(Ue,2),ze=s(Z);O(Ce=>{zr(Me,"href",n(ve).link),m(Q,n(ve).source),m(be,Ce),m(ze,n(ve).title)},[()=>n(ve).ts?c(n(ve).ts):""]),w(ie,Me)}),w(I,K)};V(B,I=>{n(i)?I(ye):n(o)?I(ee,1):n(a).length?I(_e,-1):I(ce,2)})}z("click",A,T),w(e,h),Zn()}_a(["click"]);const qc=[["Dein erster Workflow — in 3 Schritten",`
  1. Modell holen: Tab Cookbook → wähle deinen Anwendungsfall (z.B. „Coden") → Komplettes Setup installieren. Der Download läuft im Tab Aktivität.
  2. @@ -109,4 +109,4 @@ ${A}
  3. OpenWebUI: Klassische Chat-Oberfläche für Fragen/Texte.
  4. Tipp: starkes Modell (coder) für schwere Aufgaben, schnelles - (scout) für einfache — du wählst es einfach im Tool aus.

    `]],qc=[["LLM / Modell","Die KI selbst — versteht und schreibt Text (wie ChatGPT, nur lokal auf deinem PC). Ein Modell ist eine konkrete Datei davon, oft auf etwas spezialisiert (Code, Bilder, Chat)."],["GGUF","Das lokal lauffähige Dateiformat. Unsere Engine lädt nur GGUF — Repos ohne GGUF kann sie nicht nutzen."],["Quantisierung (Q4_K_M)","Verkleinert ein Modell (16-Bit → 4-Bit), damit es in den Speicher passt — bei kaum Qualitätsverlust. Q4_K_M ist der beliebte Sweetspot."],["Kontextfenster","Das Kurzzeitgedächtnis: wie viel Text sich das Modell gleichzeitig merkt. Größer = mehr Überblick, mehr Speicher, etwas langsamer."],["Auto-Swap","llama-swap lädt/entlädt Modelle automatisch — immer nur eins gleichzeitig. Du managst nichts."],["Agent","Ein Programm (OpenCode, Cline, Continue), das das Modell handeln lässt — Dateien lesen/ändern, Befehle ausführen, planen, im Loop bis die Aufgabe erledigt ist."],["MCP","Model Context Protocol — der Anschluss, über den ein Agent zusätzliche Werkzeuge bekommt (Web, Dateien, Datenbank). MCP = Werkzeuge."],["Skills","Verpackte Anleitung für wiederkehrende Aufgaben — der Agent liest sie und macht es jedes Mal wie du willst. Skills = Anleitungen."],["Rules / Commands","Die konkrete Form von Skills je Tool: AGENTS.md (OpenCode), .clinerules (Cline), Rules/Prompts (Continue)."]];var Nc=M('
    '),Ic=M('

    '),Fc=M('

    Guide für Einsteiger

    Praktische Workflows + die Local-LLM-Welt in Klartext — ohne Vorwissen.

    Tutorials & Workflows

    Konzepte kurz erklärt

    ',1);function Kc(e){var t=Fc(),a=l(ft(t),4);Ue(a,21,()=>Bc,Ge,(i,o,d)=>{var c=N(()=>Qa(n(o),2));let u=()=>n(c)[0],f=()=>n(c)[1];var g=Nc();g.open=d===0;var $=s(g),h=s($),b=l($,2);Qe(b,f,!0),z(()=>m(h,u())),x(i,g)});var r=l(a,4);Ue(r,21,()=>qc,Ge,(i,o)=>{var d=N(()=>Qa(n(o),2));let c=()=>n(d)[0],u=()=>n(d)[1];var f=Ic(),g=s(f),$=s(g),h=l(g,2),b=s(h);z(()=>{m($,c()),m(b,u())}),x(i,f)}),x(e,t)}let Hi={};function Gc(){const e=document.getElementById("top-active-text");e&&(e.classList.add("swap-flash"),setTimeout(()=>e.classList.remove("swap-flash"),900))}function Vi(e){const t=document.getElementById("swdot"),a=document.getElementById("swlabel"),r=document.getElementById("top-active-text");if(!e)t&&(t.className="dot off"),a&&(a.textContent="Backend nicht erreichbar"),r&&(r.textContent="Backend offline"),Wi("Backend nicht erreichbar – läuft uvicorn?",!1);else{const i=(e.swap_url||"").replace(/^https?:\/\//,"");t&&(t.className="dot "+(e.swap_ok?"on":"off")),a&&(a.textContent=(e.swap_ok?"LLM-Engine: Online – ":"LLM-Engine: Offline – ")+i);const o=(e.models||[]).find(c=>c.state==="running");r&&(r.innerHTML=o?`Geladen: ${o.name}`:"Kein Modell im VRAM"),e.swap_ok?Wc():Wi(`LLM-Engine nicht erreichbar unter ${i} – läuft der llama-swap Dienst?`,!0);const d={};for(const c of e.models||[])d[c.name]=c.state;for(const[c,u]of Object.entries(d)){const f=Hi[c];if(f&&f!==u&&(u==="running"||u==="loading")){Gc();break}}Hi=d}Uc(e),dr.set(e)}function Uc(e){const t=document.getElementById("sec-chip"),a=document.getElementById("sec-chip-tx");!t||!a||(t.classList.toggle("ok",!!(e!=null&&e.secured)),a.textContent=e!=null&&e.secured?"Geschützt · Heimnetz":"Nur im Heimnetz (offen)")}function Hc(e){ra.set(e||[])}function Vc(e){yi.set(e)}function Wi(e,t){const a=document.getElementById("alert");a.className="alert"+(t?" warn":""),a.innerHTML=`${e}`,a.style.display="flex"}function Wc(){const e=document.getElementById("alert");e&&(e.style.display="none")}const jc=e=>{var t;return(t=document.querySelector(`.nav-item[data-view="${e}"]`))==null?void 0:t.click()};function Ur(e,t,a,r){const i=document.createElement("span");return i.className="upd-badge"+(t?" "+t:""),i.textContent=e,r&&(i.title=r),i.onclick=()=>jc(a),i}async function js(){try{const e=await fe("/api/updates"),t=document.getElementById("update-badges");if(!t)return;const a=e.apt_cache_age_h,r=a!=null&&a>24,i=a!=null?` · apt-Cache: vor ${a}h`:"";t.innerHTML="",t.appendChild(Ur(e.os>0?`OS: ${e.os} Updates`:r?"OS: ?":"OS: aktuell",e.os>0||r?"warn":"ok","server",e.os>0?`${e.os} Pakete${i}`:r?`apt-Cache veraltet (${a}h)`:`Keine Updates${i}`)),t.appendChild(Ur(e.engine>0?"Engine: Update":"Engine: aktuell",e.engine>0?"warn":"ok","server")),t.appendChild(Ur(e.models>0?`Modelle: ${e.models}`:"Modelle: aktuell",e.models>0?"warn":"ok","news"))}catch{}}async function zr(){try{Vi(await fe("/api/status"))}catch{Vi(null)}}async function Js(){try{Hc(await fe("/api/jobs"))}catch{}}function Qs(){const e=window.location.protocol==="https:"?"wss:":"ws:",t=new WebSocket(`${e}//${window.location.host}/api/system/stream`);t.onmessage=a=>{try{Vc(JSON.parse(a.data))}catch{}},t.onclose=()=>{setTimeout(Qs,3e3)},t.onerror=()=>t.close()}var Xi;((Xi=document.getElementById("logo"))==null?void 0:Xi.innerHTML)!==void 0&&(document.getElementById("logo").innerHTML=Pr.logo);document.querySelectorAll("[data-ic]").forEach(e=>{e.innerHTML=Pr[e.dataset.ic||""]||""});const Jc=[["overview",ed],["models",hd],["activity",Ed],["server",Rd],["cookbook",vc],["connect",Ec],["news",Rc],["guides",Kc]];for(const[e,t]of Jc){const a=document.querySelector(`.view[data-view="${e}"]`);a&&wo(t,{target:a})}Fo("overview");const Hr=document.getElementById("token");Hr&&(Hr.value=Ws(),Hr.addEventListener("change",e=>{Ro(e.target.value),zr()}));const Vr=document.getElementById("hf-token");Vr&&(Vr.value=Ja(),Vr.addEventListener("change",e=>qo(e.target.value)));const ji=document.getElementById("nav-settings"),Wr=document.getElementById("settings-modal"),Ji=document.getElementById("sm-close");ji&&Wr&&Ji&&(ji.addEventListener("click",()=>Wr.style.display="flex"),Ji.addEventListener("click",()=>Wr.style.display="none"));function Qc(){const e=document.getElementById("clock");e&&(e.textContent=new Date().toTimeString().slice(0,5))}document.addEventListener("mc:refresh",zr);zr();Js();Qs();setInterval(Qc,1e3);setInterval(zr,3e3);setInterval(Js,1500);js();setInterval(js,6e4); + (scout) für einfache — du wählst es einfach im Tool aus.

    `]],Ic=[["LLM / Modell","Die KI selbst — versteht und schreibt Text (wie ChatGPT, nur lokal auf deinem PC). Ein Modell ist eine konkrete Datei davon, oft auf etwas spezialisiert (Code, Bilder, Chat)."],["GGUF","Das lokal lauffähige Dateiformat. Unsere Engine lädt nur GGUF — Repos ohne GGUF kann sie nicht nutzen."],["Quantisierung (Q4_K_M)","Verkleinert ein Modell (16-Bit → 4-Bit), damit es in den Speicher passt — bei kaum Qualitätsverlust. Q4_K_M ist der beliebte Sweetspot."],["Kontextfenster","Das Kurzzeitgedächtnis: wie viel Text sich das Modell gleichzeitig merkt. Größer = mehr Überblick, mehr Speicher, etwas langsamer."],["Auto-Swap","llama-swap lädt/entlädt Modelle automatisch — immer nur eins gleichzeitig. Du managst nichts."],["Agent","Ein Programm (OpenCode, Cline, Continue), das das Modell handeln lässt — Dateien lesen/ändern, Befehle ausführen, planen, im Loop bis die Aufgabe erledigt ist."],["MCP","Model Context Protocol — der Anschluss, über den ein Agent zusätzliche Werkzeuge bekommt (Web, Dateien, Datenbank). MCP = Werkzeuge."],["Skills","Verpackte Anleitung für wiederkehrende Aufgaben — der Agent liest sie und macht es jedes Mal wie du willst. Skills = Anleitungen."],["Rules / Commands","Die konkrete Form von Skills je Tool: AGENTS.md (OpenCode), .clinerules (Cline), Rules/Prompts (Continue)."]];var Nc=M('
    '),Fc=M('

    '),Kc=M('

    Guide für Einsteiger

    Praktische Workflows + die Local-LLM-Welt in Klartext — ohne Vorwissen.

    Tutorials & Workflows

    Konzepte kurz erklärt

    ',1);function Gc(e){var t=Kc(),a=l(ft(t),4);Ge(a,21,()=>qc,Ke,(i,o,d)=>{var c=q(()=>Ya(n(o),2));let u=()=>n(c)[0],f=()=>n(c)[1];var _=Nc();_.open=d===0;var T=s(_),h=s(T),b=l(T,2);Ye(b,f,!0),O(()=>m(h,u())),w(i,_)});var r=l(a,4);Ge(r,21,()=>Ic,Ke,(i,o)=>{var d=q(()=>Ya(n(o),2));let c=()=>n(d)[0],u=()=>n(d)[1];var f=Fc(),_=s(f),T=s(_),h=l(_,2),b=s(h);O(()=>{m(T,c()),m(b,u())}),w(i,f)}),w(e,t)}let Vi={};function Uc(){const e=document.getElementById("top-active-text");e&&(e.classList.add("swap-flash"),setTimeout(()=>e.classList.remove("swap-flash"),900))}function Wi(e){const t=document.getElementById("swdot"),a=document.getElementById("swlabel"),r=document.getElementById("top-active-text");if(!e)t&&(t.className="dot off"),a&&(a.textContent="Backend nicht erreichbar"),r&&(r.textContent="Backend offline"),ji("Backend nicht erreichbar – läuft uvicorn?",!1);else{const i=(e.swap_url||"").replace(/^https?:\/\//,"");t&&(t.className="dot "+(e.swap_ok?"on":"off")),a&&(a.textContent=(e.swap_ok?"LLM-Engine: Online – ":"LLM-Engine: Offline – ")+i);const o=(e.models||[]).find(c=>c.state==="running");r&&(r.innerHTML=o?`Geladen: ${o.name}`:"Kein Modell im VRAM"),e.swap_ok?jc():ji(`LLM-Engine nicht erreichbar unter ${i} – läuft der llama-swap Dienst?`,!0);const d={};for(const c of e.models||[])d[c.name]=c.state;for(const[c,u]of Object.entries(d)){const f=Vi[c];if(f&&f!==u&&(u==="running"||u==="loading")){Uc();break}}Vi=d}Hc(e),dr.set(e)}function Hc(e){const t=document.getElementById("sec-chip"),a=document.getElementById("sec-chip-tx");!t||!a||(t.classList.toggle("ok",!!(e!=null&&e.secured)),a.textContent=e!=null&&e.secured?"Geschützt · Heimnetz":"Nur im Heimnetz (offen)")}function Vc(e){ra.set(e||[])}function Wc(e){xi.set(e)}function ji(e,t){const a=document.getElementById("alert");a.className="alert"+(t?" warn":""),a.innerHTML=`${e}`,a.style.display="flex"}function jc(){const e=document.getElementById("alert");e&&(e.style.display="none")}const Jc=e=>{var t;return(t=document.querySelector(`.nav-item[data-view="${e}"]`))==null?void 0:t.click()};function Hr(e,t,a,r){const i=document.createElement("span");return i.className="upd-badge"+(t?" "+t:""),i.textContent=e,r&&(i.title=r),i.onclick=()=>Jc(a),i}async function js(){try{const e=await fe("/api/updates"),t=document.getElementById("update-badges");if(!t)return;const a=e.apt_cache_age_h,r=a!=null&&a>24,i=a!=null?` · apt-Cache: vor ${a}h`:"";t.innerHTML="",t.appendChild(Hr(e.os>0?`OS: ${e.os} Updates`:r?"OS: ?":"OS: aktuell",e.os>0||r?"warn":"ok","server",e.os>0?`${e.os} Pakete${i}`:r?`apt-Cache veraltet (${a}h)`:`Keine Updates${i}`)),t.appendChild(Hr(e.engine>0?"Engine: Update":"Engine: aktuell",e.engine>0?"warn":"ok","server")),t.appendChild(Hr(e.models>0?`Modelle: ${e.models}`:"Modelle: aktuell",e.models>0?"warn":"ok","news"))}catch{}}async function Dr(){try{Wi(await fe("/api/status"))}catch{Wi(null)}}async function Js(){try{Vc(await fe("/api/jobs"))}catch{}}function Qs(){const e=window.location.protocol==="https:"?"wss:":"ws:",t=new WebSocket(`${e}//${window.location.host}/api/system/stream`);t.onmessage=a=>{try{Wc(JSON.parse(a.data))}catch{}},t.onclose=()=>{setTimeout(Qs,3e3)},t.onerror=()=>t.close()}var es;((es=document.getElementById("logo"))==null?void 0:es.innerHTML)!==void 0&&(document.getElementById("logo").innerHTML=Pr.logo);document.querySelectorAll("[data-ic]").forEach(e=>{e.innerHTML=Pr[e.dataset.ic||""]||""});const Qc=[["overview",td],["models",gd],["activity",Td],["server",Bd],["cookbook",fc],["connect",Tc],["news",Bc],["guides",Gc]];for(const[e,t]of Qc){const a=document.querySelector(`.view[data-view="${e}"]`);a&&yo(t,{target:a})}Ko("overview");const Vr=document.getElementById("token");Vr&&(Vr.value=Ws(),Vr.addEventListener("change",e=>{Bo(e.target.value),Dr()}));const Wr=document.getElementById("hf-token");Wr&&(Wr.value=Qa(),Wr.addEventListener("change",e=>Io(e.target.value)));const Ji=document.getElementById("nav-settings"),jr=document.getElementById("settings-modal"),Qi=document.getElementById("sm-close");Ji&&jr&&Qi&&(Ji.addEventListener("click",()=>jr.style.display="flex"),Qi.addEventListener("click",()=>jr.style.display="none"));function Yc(){const e=document.getElementById("clock");e&&(e.textContent=new Date().toTimeString().slice(0,5))}document.addEventListener("mc:refresh",Dr);Dr();Js();Qs();setInterval(Yc,1e3);setInterval(Dr,3e3);setInterval(Js,1500);js();setInterval(js,6e4); diff --git a/static/js/main.js b/static/js/main.js deleted file mode 100644 index a6c9f2f..0000000 --- a/static/js/main.js +++ /dev/null @@ -1,195 +0,0 @@ -// main.js — App-Boot: Panels mounten, Nav starten, Topbar/Alert pflegen, Polling fahren. -// Panel-Vertrag: { id, mount?(), onStatus?(s), onJobs?(jobs) }. - -import { api, getToken, setToken, getHfToken, setHfToken } from "./core/api.js"; -import { $ } from "./core/ui.js"; -import { initNav } from "./core/nav.js"; - -import overview from "./panels/overview.js"; -import models from "./panels/models.js"; -import server from "./panels/server.js"; -import jobs from "./panels/jobs.js"; -import cookbook from "./panels/cookbook.js"; -import connect from "./panels/connect.js"; -import news from "./panels/news.js"; -import guides from "./panels/guides.js"; - -const panels = [overview, models, server, jobs, cookbook, connect, news, guides]; - -let lastJobs = []; -let lastSystem = null; -let prevModelStates = {}; - -// ---- Topbar / Alert aus dem Status ableiten ---- -function applyStatus(s) { - const dot = $("#swdot"), label = $("#swlabel"), alert = $("#alert"); - - if (!s) { - dot.className = "dot off"; - label.textContent = "Backend nicht erreichbar"; - $("#top-active-text").textContent = "Backend offline"; - showAlert("Backend nicht erreichbar – läuft uvicorn?", false); - } else { - const host = s.swap_url.replace(/^https?:\/\//, ""); - dot.className = "dot " + (s.swap_ok ? "on" : "off"); - label.textContent = (s.swap_ok ? "LLM-Engine: Online – " : "LLM-Engine: Offline – ") + host; - - const active = (s.models || []).find(m => m.state === "running"); - if (active) { - $("#top-active-text").innerHTML = `Geladen: ${active.name}`; - } else { - $("#top-active-text").innerHTML = "Kein Modell im VRAM"; - } - - if (s.swap_ok) hideAlert(); - else showAlert(`LLM-Engine nicht erreichbar unter ${host} – läuft der llama-swap Dienst?`, true); - } - setSecChip(s); - for (const p of panels) p.onStatus?.(s); -} - -// Security-Chip: ehrlich anzeigen, ob ein Zugangs-Token aktiv ist. -function setSecChip(s) { - const chip = $("#sec-chip"), tx = $("#sec-chip-tx"); - if (!chip || !tx) return; - const secured = !!s?.secured; - chip.classList.toggle("ok", secured); - tx.textContent = secured ? "Geschützt · Heimnetz" : "Nur im Heimnetz (offen)"; -} - -function applyJobs(jobs) { - lastJobs = jobs || []; - for (const p of panels) p.onJobs?.(lastJobs); -} - -function applySystem(sys) { - lastSystem = sys; - for (const p of panels) p.onSystem?.(sys); -} - -function showAlert(html, warn) { - const a = $("#alert"); - a.className = "alert" + (warn ? " warn" : ""); - a.innerHTML = `${html}`; - a.style.display = "flex"; -} -function hideAlert() { $("#alert").style.display = "none"; } - -// ---- Toolbar: ausstehende Updates anzeigen ---- -const goView = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click(); -function badge(text, cls, view, title) { - const s = document.createElement("span"); - s.className = "upd-badge" + (cls ? " " + cls : ""); - s.textContent = text; - if (title) s.title = title; - s.onclick = () => goView(view); - return s; -} -async function pollUpdates() { - try { - const u = await api("/api/updates"); - const el = $("#update-badges"); if (!el) return; - const age = u.apt_cache_age_h; - const stale = age != null && age > 24; - const ageNote = age != null ? ` · apt-Cache: vor ${age}h` : ""; - el.innerHTML = ""; - el.appendChild(badge( - u.os > 0 ? `OS: ${u.os} Updates` : (stale ? "OS: ?" : "OS: aktuell"), - u.os > 0 ? "warn" : stale ? "warn" : "ok", - "server", - u.os > 0 ? `${u.os} ausstehende Pakete${ageNote}` : (stale ? `apt-Cache veraltet (${age}h) — OS-Update prüfen` : `Keine ausstehenden Pakete${ageNote}`) - )); - el.appendChild(badge(u.engine > 0 ? "Engine: Update" : "Engine: aktuell", u.engine > 0 ? "warn" : "ok", "server")); - el.appendChild(badge(u.models > 0 ? `Modelle: ${u.models}` : "Modelle: aktuell", u.models > 0 ? "warn" : "ok", "news")); - } catch { /* still */ } -} - -// ---- Swap-Erkennung: kurze visuelle Hervorhebung wenn Modell wechselt ---- -function flashSwap() { - const el = $("#top-active-text"); - if (el) { el.classList.add("swap-flash"); setTimeout(() => el.classList.remove("swap-flash"), 900); } -} - -// ---- Polling ---- -async function pollStatus() { - try { - const s = await api("/api/status"); - // Zustandswechsel erkennen → Swap-Flash auslösen - const newStates = {}; - for (const m of (s?.models || [])) newStates[m.name] = m.state; - for (const [name, state] of Object.entries(newStates)) { - const prev = prevModelStates[name]; - if (prev && prev !== state && (state === "running" || state === "loading")) { - flashSwap(); - break; - } - } - prevModelStates = newStates; - applyStatus(s); - } - catch { applyStatus(null); } -} -async function pollJobs() { - try { applyJobs(await api("/api/jobs")); } - catch { /* still */ } -} - -function connectSystemStream() { - const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; - const wsUrl = `${protocol}//${window.location.host}/api/system/stream`; - const ws = new WebSocket(wsUrl); - - ws.onmessage = (e) => { - try { - const data = JSON.parse(e.data); - applySystem(data); - } catch (err) {} - }; - - ws.onclose = () => { - // Bei Verbindungsabbruch nach 3 Sekunden erneut versuchen - setTimeout(connectSystemStream, 3000); - }; - - ws.onerror = () => ws.close(); -} - -// ---- Boot ---- -function bootToken() { - const i = $("#token"); - if (i) { - i.value = getToken(); - i.addEventListener("change", e => { setToken(e.target.value); pollStatus(); }); - } - - const hf = $("#hf-token"); - if (hf) { hf.value = getHfToken(); hf.addEventListener("change", e => setHfToken(e.target.value)); } - - const sbtn = $("#nav-settings"); - const smod = $("#settings-modal"); - const scls = $("#sm-close"); - if (sbtn && smod && scls) { - sbtn.addEventListener("click", () => smod.style.display = "flex"); - scls.addEventListener("click", () => smod.style.display = "none"); - } -} -function tickClock() { - const c = $("#clock"); - if (c) c.textContent = new Date().toTimeString().slice(0, 5); -} - -for (const p of panels) p.mount?.(); -initNav("overview"); -bootToken(); -tickClock(); -document.addEventListener("mc:refresh", pollStatus); - -pollStatus(); -pollJobs(); -connectSystemStream(); - -setInterval(tickClock, 1000); -setInterval(pollStatus, 3000); -setInterval(pollJobs, 1500); -pollUpdates(); -setInterval(pollUpdates, 60000); diff --git a/static/js/panels/connect.js b/static/js/panels/connect.js deleted file mode 100644 index 0971591..0000000 --- a/static/js/panels/connect.js +++ /dev/null @@ -1,308 +0,0 @@ -// connect.js — „Verbinden" (v5): Wizard-Style, Merge-sichere Configs, Auto-Swap erklärt. - -import { api, getToken } from "../core/api.js"; -import { $, esc, icon, toast } from "../core/ui.js"; - -let S = null; -let lastSig = ""; -let selectedTool = "zed"; -let selectedModelIds = []; - -window.mcCopyCode = btn => { - const code = btn.parentElement.querySelector("code,pre"); - navigator.clipboard?.writeText(code?.textContent || ""); - const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1400); -}; -window.mcCopyVal = btn => { - const i = btn.previousElementSibling; - navigator.clipboard?.writeText(i.value); - const o = btn.textContent; btn.textContent = "Kopiert!"; setTimeout(() => (btn.textContent = o), 1400); -}; -window.cnGo = v => document.querySelector(`.nav-item[data-view="${v}"]`)?.click(); -window.cnSelectTool = t => { selectedTool = t; renderGuide(); }; -window.cnToggleModel = id => { - if (selectedModelIds.includes(id)) selectedModelIds = selectedModelIds.filter(x => x !== id); - else selectedModelIds.push(id); - renderGuide(); -}; - -function baseUrl() { return `http://${location.hostname}:8080/v1`; } -function isLanIp(h) { return /^\d{1,3}(\.\d{1,3}){3}$/.test(h); } -function behindProxy() { return location.protocol === "https:" || !isLanIp(location.hostname); } -function models() { return (S?.models || []).filter(m => !m.incomplete); } -function activeIds() { - return selectedModelIds.length ? selectedModelIds : models().map(m => m.name); -} - -function field(label, value) { - return ` -
    - - -
    `; -} - -const okChip = txt => - `${icon("check")}${esc(txt)}`; - -// ---- Merge-sichere Config-Snippets ---- - -function zedMergeBlock(url, ids) { - const ms = ids.length - ? models().filter(m => ids.includes(m.name)) - : (models().length ? models() : [{ name: "coder", meta: {} }]); - const entries = ms.map(m => { - const img = (m.meta?.caps || []).some(c => /Bild|Vision/i.test(c)); - const caps = img ? `, "capabilities": { "tools": true, "images": true }` : ``; - return ` { "name": "${m.name}", "display_name": "${m.name}", "max_tokens": 32768${caps} }`; - }).join(",\n"); - return `"language_models": { - "openai_compatible": { - "bosgame": { - "api_url": "${url}", - "available_models": [ -${entries} - ] - } - } -}`; -} - -function openCodeMergeBlock(url, ids) { - const list = ids.length ? ids : (models().length ? models().map(m => m.name) : ["coder"]); - const modelMap = list.map(id => ` "${id}": { "name": "${id}" }`).join(",\n"); - return `"provider": { - "llama-swap": { - "npm": "@ai-sdk/openai-compatible", - "name": "Bosgame (llama-swap)", - "options": { - "baseURL": "${url}", - "apiKey": "local" - }, - "models": { -${modelMap} - } - } -}`; -} - -// ---- Render-Abschnitte ---- - -function renderConnectionCard(url) { - return `
    -

    1 · Verbindung prüfen

    -
    Stelle sicher, dass die LLM-Engine erreichbar ist, bevor du Tools konfigurierst.
    - ${field("Engine-Adresse (für alle Tools)", url)} - ${behindProxy() ? `
    - Du öffnest Mission Control über einen Proxy/Namen (HTTPS o.ä.). Die Engine läuft auf Port 8080 ohne HTTPS direkt am Bosgame. - Trag in deinen Tools die LAN-IP ein: http://192.168.178.151:8080/v1 — nicht den Proxy-Namen.
    ` : ""} -
    - - -
    -
    `; -} - -function renderAutoSwapCard() { - return `
    -

    Wie funktioniert das Auto-Swap?

    -
    -
    - -
    -
    Kein manuelles Starten nötig
    -
    Wenn du in Zed oder OpenCode „coder" auswählst und eine Anfrage sendest, - lädt llama-swap das Modell automatisch — in ca. 2–5 Sekunden. Du siehst es hier im Dashboard unter „Aktives Modell".
    -
    -
    -
    - 🔄 -
    -
    Modelle wechseln sich automatisch ab
    -
    Rufst du ein anderes Modell auf, entlädt llama-swap das aktuelle (nach TTL-Ablauf) - und lädt das neue. Es läuft immer nur eines gleichzeitig — llama-swap verwaltet das komplett selbst.
    -
    -
    -
    - 🧠 -
    -
    Zwei Modelle gleichzeitig aktiv halten
    -
    Du hast 124 GB RAM — genug für z.B. Scout 8B (~5 GB) dauerhaft geladen + ein - größeres Modell das swappt. Dafür beim Scout im Modelle-Tab → Konfigurieren die TTL auf 99999 setzen - (= nie automatisch entladen). Das kleine Modell beantwortet schnelle Fragen sofort, ohne jede Wartezeit.
    -
    -
    -
    -
    `; -} - -function renderModelSelector() { - const ms = models(); - if (!ms.length) return ""; - const chips = ms.map(m => - `${esc(m.name)}` - ).join(" "); - const note = selectedModelIds.length - ? `${selectedModelIds.length} von ${ms.length} ausgewählt` - : "alle werden eingeschlossen"; - return `
    -

    2 · Modelle auswählen

    ${note}
    -
    Wähle, welche Modelle im Config-Snippet erscheinen sollen. Standardmäßig alle.
    -
    ${chips}
    - ${selectedModelIds.length ? `` : ""} -
    `; -} -window.cnClearModels = () => { selectedModelIds = []; renderGuide(); }; - -function toolBtn(id, label, desc) { - const active = selectedTool === id; - return ``; -} - -function renderToolPicker() { - return `
    -

    3 · Tool wählen

    -
    Für welches Werkzeug brauchst du die Konfiguration?
    -
    - ${toolBtn("zed", "Zed", "Schneller nativer Editor — Empfohlen")} - ${toolBtn("opencode", "OpenCode", "Open-Source Coding-Agent (Terminal/Desktop)")} - ${toolBtn("cline", "Cline / Cursor", "VS Code Extension, Bring-Your-Own-Key")} - ${toolBtn("openwebui", "OpenWebUI", "Browser-Chat-Interface")} -
    -
    `; -} - -function renderConfigCard(url) { - const ids = activeIds(); - const firstModel = ids[0] || "coder"; - let content = ""; - - if (selectedTool === "zed") { - const block = zedMergeBlock(url, ids); - content = ` -

    Öffne die Zed-Settings mit Cmd/Strg + , und füge diesen Block in deine bestehende - settings.json ein. Falls bereits ein "bosgame"-Eintrag existiert: vorher löschen.

    -
    - Nur diesen Block einfügen, nicht die ganze Datei ersetzen — sonst gehen andere Einstellungen verloren. -
    -
    - -
    ${esc(block)}
    -
    -

    API-Key: Öffne den Agent-Panel → Anbieter bosgame → trag irgendeinen Wert ein (z.B. local). - Alternativ BOSGAME_API_KEY=local als Umgebungsvariable.

    -
    Danach im Agent-Panel das Modell bosgame / ${esc(firstModel)} auswählen. Das Auto-Swap lädt es beim ersten Request.
    `; - } else if (selectedTool === "opencode") { - const block = openCodeMergeBlock(url, ids); - content = ` -

    Datei anlegen oder öffnen:

    -

    Windows: %USERPROFILE%\\.config\\opencode\\opencode.json
    - Linux/Mac: ~/.config/opencode/opencode.json
    - Oder im Projektordner: opencode.json

    -
    - Nur den "provider"-Block einfügen oder mergen. - Falls bereits ein "llama-swap"-Eintrag vorhanden ist: vorher löschen, sonst erscheinen Modelle doppelt. -
    -
    - -
    ${esc(block)}
    -
    -

    Danach in OpenCode /modelsllama-swap/${esc(firstModel)} wählen.

    -
    Statt alles zu ersetzen: nur den provider-Block in deine bestehende Config einfügen — so bleiben andere Anbieter erhalten.
    `; - } else if (selectedTool === "cline") { - content = ` -

    In Cline/Cursor den Provider „OpenAI Compatible" wählen und folgende Felder eintragen:

    - ${field("Basis-URL (OpenAI Compatible)", url)} - ${field("Modell-ID", firstModel)} -
    API-Key: ein beliebiger Wert (z.B. local) — llama-swap prüft ihn nicht.
    `; - } else if (selectedTool === "openwebui") { - content = ` -

    Settings → Admin Panel → Connections → neue OpenAI-Verbindung:

    - ${field("Basis-URL", url)} - ${field("API-Key", "dummy-key")} -
    Nach dem Speichern erscheinen die Modelle automatisch in der Modell-Auswahl.
    `; - } - - return `
    -

    4 · Config-Snippet

    - ${content} -
    `; -} - -function renderVisionCard(url) { - const visionModel = models().find(m => m.meta?.caps?.includes("Bild"))?.name; - if (!visionModel) return ""; - const snippet = `curl ${url}/chat/completions \\ - -H "Content-Type: application/json" \\ - -d '{ - "model": "${visionModel}", - "messages": [{"role":"user","content":[ - {"type":"text","text":"Was ist auf dem Bild falsch?"}, - {"type":"image_url","image_url":{"url":"data:image/png;base64,"}} - ]}] -}'`; - return `
    -

    Bilder einbinden (Vision)

    ${okChip(`„${visionModel}" bereit`)}
    -
    Mit Vision-Modellen kannst du Screenshots oder Fehlerbilder direkt mitschicken.
    -
    - -
    ${esc(snippet)}
    -
    -
    `; -} - -function render() { - const c = $(".view[data-view='connect']"); if (!c) return; - const url = baseUrl(); - - c.innerHTML = ` -
    -

    Verbinden

    -
    Schritt für Schritt: KI-Modelle in deine Tools einbinden — getestet, erklärt, mit Copy-Paste-Config.
    -
    - - ${renderConnectionCard(url)} - ${renderAutoSwapCard()} - ${renderModelSelector()} - ${renderToolPicker()} - ${renderConfigCard(url)} - ${renderVisionCard(url)}`; - - $("#cn-test")?.addEventListener("click", testConnection); -} - -function renderGuide() { - const url = baseUrl(); - const sel = $(".view[data-view='connect'] .card:nth-of-type(3)"); - if (!sel) { render(); return; } - // Nur die unteren 3 Karten neu rendern (Modell-Auswahl, Tool-Picker, Config) - const c = $(".view[data-view='connect']"); - const cards = c?.querySelectorAll(".card"); - if (!cards || cards.length < 3) { render(); return; } - // Einfachster Weg: komplett neu rendern (Scroll-Position bleibt wegen Hash-Routing ok) - render(); -} - -async function testConnection() { - const res = $("#cn-test-result"); res.innerHTML = `Teste…`; - try { - const r = await api("/api/integration/test"); - res.innerHTML = r.ok - ? okChip(`Verbunden — ${r.models.length} Modell(e): ${r.models.join(", ")}`) - : `✗ Keine Verbindung: ${esc(r.error)}`; - } catch (e) { res.innerHTML = `✗ ${esc(e.message)}`; } -} - -function mount() { render(); } -function onStatus(s) { - S = s; - const sig = (s?.models || []).map(m => m.name).join(",") + "|" + !!s; - if (sig !== lastSig) { lastSig = sig; render(); } -} - -export default { id: "connect", mount, onStatus }; diff --git a/static/js/panels/cookbook.js b/static/js/panels/cookbook.js deleted file mode 100644 index 1e38b4a..0000000 --- a/static/js/panels/cookbook.js +++ /dev/null @@ -1,585 +0,0 @@ -// cookbook.js — Cookbook 2.0 (v4): Use-Case-Setups („Wofür?") + Profi-Suche. -// Backend: /api/cookbook/{recipes,install-recipe,analyze,evaluate} (hw_math). - -import { api, getHfToken } from "../core/api.js"; -import { $, esc, icon, toast, infoDot, confirmModal } from "../core/ui.js"; - -const CTX_HELP = "Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr, braucht aber mehr Speicher und wird etwas langsamer."; -const GGUF_HELP = "GGUF ist das lokal lauffähige Dateiformat. Unsere Engine lädt nur GGUF — Repos ohne GGUF-Datei kann sie nicht nutzen."; -const MOE_HELP = "MoE (Mixture of Experts): viele Parameter fürs Wissen, aber pro Wort ist nur ein kleiner Teil aktiv → fast große-Modell-Qualität bei hohem Tempo. Ideal für deine APU."; - -// Was kann das Modell? (Fähigkeit aus dem Namen / der Backend-Rolle ableiten.) -const ROLE_LABEL = { vision: "Bilder", coder: "Coden", reasoning: "Logik", agent: "Agenten & Tools", - scout: "Allrounder", reviewer: "Review", manager: "Manager" }; -const CAP_KW = [ - { label: "Bilder", kw: ["-vl-", "-vl", "vision", "llava", "multimodal", "-mm-", "pixtral"] }, - { label: "Coden", kw: ["coder", "-code-", "code-", "codestral", "starcoder"] }, - { label: "Logik", kw: ["-r1", "deepseek-r1", "reasoning", "qwq", "magistral", "-think", "thinking", "-o1"] }, - { label: "Agenten & Tools", kw: ["hermes", "-tool", "command-r", "watt", "-fc-", "function"] }, -]; -function capLabel(name) { - const l = (name || "").toLowerCase(); - for (const c of CAP_KW) if (c.kw.some(k => l.includes(k))) return c.label; - return "Allrounder"; -} -// MoE-Erkennung über strukturelle Marker im Namen (zuverlässig, keine Falschaussagen): -// 8x7B-Stil, „…-A3B" (aktive Parameter), oder explizit „mixtral"/„moe". -function isMoe(s = "") { - const l = s.toLowerCase(); - return /\d+x\d+(\.\d+)?b/.test(l) || /(?MoE${a ? ` · ~${a}B aktiv` : ""}`; -} -function capChip(label) { return label ? `${esc(label)}` : ""; } - -const FILTERS = [ - { id: "", label: "Alle" }, { id: "coder", label: "Coder" }, { id: "scout", label: "Scout" }, - { id: "vision", label: "Vision" }, { id: "manager", label: "Manager" }, { id: "reviewer", label: "Reviewer" }, -]; - -let lastSys = null; -let currentResults = []; -let currentAnalysis = null; -let activeFilter = ""; -let RECIPES = []; -let RECOMMENDED = null; -let cardFits = []; // Fit-Level je Such-Karte (fuer „Beste Wahl"-Markierung) -const FIT_RANK = { perfect: 0, marginal: 1, too_tight: 2 }; - -const fitCls = lvl => lvl === "perfect" ? "ok" : lvl === "marginal" ? "warn" : "bad"; -const fitWord = lvl => lvl === "perfect" ? "Passt locker" : lvl === "marginal" ? "Wird knapp" : "Zu groß"; - -function mount() { - const c = $(".view[data-view='cookbook']"); - c.innerHTML = ` -
    -

    Modell-Cookbook

    -
    Wofür willst du es nutzen? Wir empfehlen das passende Setup für deine Hardware.
    -
    - -

    Wofür möchtest du es nutzen?

    - deine Hardware -
    -
    -
    Lade Setups…
    -
    - -

    Aktuell beste Modelle für dein System

    - -
    -
    Automatisch aus vertrauenswürdigen Quellen (HuggingFace) — laufend aktuell, gefiltert auf das, was auf deine Hardware passt.
    -
    Lade aktuelle Empfehlungen…
    - -
    - Profi-Modus: HuggingFace direkt durchsuchen -
    -
    - - -
    -
    -
    - -
    -
    - passt locker - läuft, aber knapp - zu groß für deinen Speicher -
    -
    Es werden nur Modelle im GGUF-Format ${infoDot(GGUF_HELP)} gezeigt — nur die laufen lokal.
    -
    -
    -
    - - - - - - `; - - renderFilters(); - $("#cb-btn-search").addEventListener("click", doSearch); - $("#cb-search").addEventListener("keydown", e => { if (e.key === "Enter") doSearch(); }); - $("#cb-sort").addEventListener("change", () => { if ($("#cb-search").value.trim() || activeFilter) doSearch(); }); - $("#cb-modal-close").addEventListener("click", () => $("#cb-modal").style.display = "none"); - $("#cb-modal").addEventListener("click", e => { if (e.target.id === "cb-modal") $("#cb-modal").style.display = "none"; }); - $("#cb-m-download").addEventListener("click", doDownload); - $("#cb-m-files").addEventListener("change", updateLiveFit); - $("#cb-m-ctx").addEventListener("change", reanalyzeCtx); - window.cbSetCtx = v => { $("#cb-m-ctx").value = v; reanalyzeCtx(); }; - $("#cb-r-close").addEventListener("click", () => $("#cb-recipe-modal").style.display = "none"); - $("#cb-recipe-modal").addEventListener("click", e => { if (e.target.id === "cb-recipe-modal") $("#cb-recipe-modal").style.display = "none"; }); - $("#cb-new-open").addEventListener("click", openNewRecipe); - $("#cb-new-close").addEventListener("click", () => $("#cb-new-modal").style.display = "none"); - $("#cb-new-modal").addEventListener("click", e => { if (e.target.id === "cb-new-modal") $("#cb-new-modal").style.display = "none"; }); - $("#cb-new-add").addEventListener("click", () => $("#cb-new-models").insertAdjacentHTML("beforeend", newModelRow())); - $("#cb-new-models").addEventListener("click", e => { const d = e.target.closest(".cb-nm-del"); if (d) d.closest(".cb-nm-row").remove(); }); - $("#cb-new-save").addEventListener("click", saveNewRecipe); - - $("#cb-disc-refresh").addEventListener("click", () => loadDiscover(true)); - $("#cb-discover").addEventListener("click", e => { - const b = e.target.closest("[data-inst]"); if (!b) return; - installDiscovered(b.getAttribute("data-inst"), b.getAttribute("data-role"), b.getAttribute("data-pb"), b); - }); - - renderHwChip(); - loadRecipes(); - loadDiscover(); -} - -// ---- Automatische Modell-Entdeckung („aktuell beste Modelle") ---- -function fmtAgo(ts) { - const s = Date.now() / 1000 - ts; - if (s < 90) return "gerade eben"; - if (s < 3600) return `vor ${Math.round(s / 60)} min`; - if (s < 86400) return `vor ${Math.round(s / 3600)} h`; - return `vor ${Math.round(s / 86400)} Tg.`; -} - -async function loadDiscover(force = false) { - const box = $("#cb-discover"), when = $("#cb-disc-when"), btn = $("#cb-disc-refresh"); - if (force) { btn.disabled = true; btn.textContent = "Suche…"; box.innerHTML = `
    Frage Quellen ab…
    `; } - try { - const d = await api("/api/cookbook/discover" + (force ? "?force=true" : "")); - if (when) when.textContent = d.updated ? `aktualisiert ${fmtAgo(d.updated)}` : ""; - renderDiscover(d); - } catch (e) { - box.innerHTML = `
    Empfehlungen nicht ladbar: ${esc(e.message)}
    `; - } - btn.disabled = false; btn.textContent = "Aktualisieren"; -} - -function renderDiscover(d) { - const box = $("#cb-discover"); - const cats = d.categories || []; - if (!cats.length) { box.innerHTML = `
    Keine passenden Modelle gefunden.
    `; return; } - box.innerHTML = cats.map(c => ` -
    -
    ${icon(c.icon)} -

    ${esc(c.title)}

    -
    ${c.models.map(m => discCard(m, c.recommended)).join("")}
    -
    `).join(""); -} - -function discCard(m, recommended) { - const reqs = (m.requirements || []).map(r => - `
    ⚠ ${esc(r)}
    `).join(""); - const best = recommended && m.repo === recommended; - const tag = best ? `
    ★ Beste Wahl für dein System
    ` : ""; - const chips = `
    ${capChip(ROLE_LABEL[m.role] || capLabel(m.repo))}${moeChip(m.repo)}
    `; - const action = m.installed - ? `✓ installiert` - : ``; - return `
    - ${tag} -
    -

    ${esc(m.name)}

    -
    ${esc(m.author)} · ${m.params_b}B
    - ${esc(m.fit.text)} -
    - ${chips} -
    ${metricLine(m.fit)} · ⬇ ${(m.downloads || 0).toLocaleString()}
    - ${reqs} -
    -
    ${action}
    -
    `; -} - -async function installDiscovered(repo, role, params_b, btn) { - btn.disabled = true; btn.textContent = "Starte…"; - try { - await api("/api/cookbook/install-model", { method: "POST", - body: JSON.stringify({ repo, role, params_b: parseFloat(params_b) || 7, quant: "Q4_K_M", hf_token: getHfToken() }) }); - toast("Download gestartet — siehe Aktivität."); - document.querySelector(".nav-item[data-view='activity']")?.click(); - } catch (e) { toast("Fehler: " + e.message, true); btn.disabled = false; btn.textContent = "Installieren"; } -} - -// ---- Use-Case-Setups ---- -async function loadRecipes() { - try { - const d = await api("/api/cookbook/recipes"); - RECIPES = d.recipes || []; - RECOMMENDED = d.recommended_id || null; - renderRecipes(); - } catch (e) { - $("#cb-recipes").innerHTML = `
    Setups nicht ladbar: ${esc(e.message)}
    `; - } -} - -function renderRecipes() { - $("#cb-recipes").innerHTML = RECIPES.map(r => { - const best = r.id === RECOMMENDED; - const tag = best ? `
    ★ Beste Wahl für dein System
    ` - : r.user ? `
    Dein Setup
    ` : ""; - const del = r.user ? ` - - ×` : ""; - return ``; - }).join(""); - $("#cb-recipes").querySelectorAll("[data-recipe]").forEach(b => - b.addEventListener("click", () => openRecipe(b.getAttribute("data-recipe")))); - $("#cb-recipes").querySelectorAll("[data-delr]").forEach(x => - x.addEventListener("click", e => { e.stopPropagation(); deleteRecipe(x.getAttribute("data-delr")); })); - $("#cb-recipes").querySelectorAll("[data-editr]").forEach(x => - x.addEventListener("click", e => { e.stopPropagation(); openEditRecipe(x.getAttribute("data-editr")); })); -} - -// ---- Eigenes Setup erstellen/löschen ---- -function newModelRow() { - return `
    - - - - -
    `; -} - -let editRecipeId = null; - -function openNewRecipe() { - editRecipeId = null; - $("#cb-new-title").value = ""; - $("#cb-new-desc").value = ""; - $("#cb-new-models").innerHTML = newModelRow(); - $("#cb-new-modal-title").textContent = "Eigenes Setup erstellen"; - $("#cb-new-save").textContent = "Setup speichern"; - $("#cb-new-modal").style.display = "flex"; -} - -function openEditRecipe(id) { - const r = RECIPES.find(x => x.id === id); if (!r) return; - editRecipeId = id; - $("#cb-new-title").value = r.title; - $("#cb-new-desc").value = r.desc || ""; - $("#cb-new-models").innerHTML = r.models.map(m => { - const row = document.createElement("div"); - row.innerHTML = newModelRow(); - const el = row.firstElementChild; - el.querySelector(".cb-nm-repo").value = m.repo; - el.querySelector(".cb-nm-role").value = m.role; - const quant = el.querySelector(".cb-nm-quant"); - if (quant) [...quant.options].forEach(o => { if (o.value === m.quant) o.selected = true; }); - return el.outerHTML; - }).join(""); - $("#cb-new-modal-title").textContent = "Setup bearbeiten"; - $("#cb-new-save").textContent = "Änderungen speichern"; - $("#cb-new-modal").style.display = "flex"; -} - -async function saveNewRecipe() { - const title = $("#cb-new-title").value.trim(); - if (!title) return toast("Bitte einen Titel angeben.", true); - const models = [...$("#cb-new-models").querySelectorAll(".cb-nm-row")].map(row => ({ - repo: row.querySelector(".cb-nm-repo").value.trim(), - role: row.querySelector(".cb-nm-role").value.trim() || "modell", - quant: row.querySelector(".cb-nm-quant").value, - })).filter(m => m.repo); - if (!models.length) return toast("Mindestens ein Modell (Repo) angeben.", true); - const btn = $("#cb-new-save"); btn.disabled = true; btn.textContent = "Speichere…"; - try { - const body = JSON.stringify({ title, desc: $("#cb-new-desc").value.trim(), models }); - if (editRecipeId) { - await api("/api/cookbook/user-recipe/" + encodeURIComponent(editRecipeId), { method: "PUT", body }); - toast("Setup aktualisiert."); - } else { - await api("/api/cookbook/user-recipe", { method: "POST", body }); - toast("Setup gespeichert."); - } - editRecipeId = null; - $("#cb-new-modal").style.display = "none"; - loadRecipes(); - } catch (e) { toast("Fehler: " + e.message, true); } - btn.disabled = false; btn.textContent = editRecipeId ? "Änderungen speichern" : "Setup speichern"; -} - -async function deleteRecipe(id) { - const r = RECIPES.find(x => x.id === id); - if (!await confirmModal({ title: `„${r ? r.title : id}" löschen?`, - body: "Dein eigenes Setup wird aus dem Cookbook entfernt. Bereits installierte Modelle bleiben unangetastet.", - confirmLabel: "Löschen", danger: true })) return; - try { await api("/api/cookbook/user-recipe/" + encodeURIComponent(id), { method: "DELETE" }); toast("Setup gelöscht."); loadRecipes(); } - catch (e) { toast(e.message, true); } -} - -function openRecipe(id) { - const r = RECIPES.find(x => x.id === id); if (!r) return; - $("#cb-r-title").textContent = r.title; - $("#cb-r-desc").textContent = r.desc; - $("#cb-r-models").innerHTML = `
    ${r.models.map(m => ` -
    -
    -
    - ${esc(m.name)}${esc(m.role)}${moeChip(m.repo)}
    -
    ${esc(m.why)}
    -
    ~${m.fit.req_gb.toFixed(1)} GB · ~${Math.round(m.fit.tps)} Tok/s · optimal ~${Math.round(m.optimal_ctx / 1024)}k Kontext
    -
    - ${esc(m.fit.text)} -
    `).join("")}
    `; - const maxRam = Math.max(...r.models.map(m => m.fit.req_gb)); - $("#cb-r-note").innerHTML = `Größter Spitzenbedarf: ~${maxRam.toFixed(1)} GB. ` + - `Es läuft immer nur ein Modell gleichzeitig — das größte bestimmt, ob das Setup passt (kein Summieren).`; - const btn = $("#cb-r-install"); - btn.className = r.fit_level === "too_tight" ? "primary warn" : "primary"; - btn.textContent = r.fit_level === "too_tight" ? "Trotzdem installieren (zu groß)" : "Komplettes Setup installieren"; - btn.onclick = () => installRecipe(r.id); - $("#cb-recipe-modal").style.display = "flex"; -} - -async function installRecipe(id) { - const btn = $("#cb-r-install"); 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(`Setup wird installiert (${r.count} Modelle) — siehe Aktivität.`); - $("#cb-recipe-modal").style.display = "none"; - document.querySelector(".nav-item[data-view='activity']")?.click(); - } catch (e) { toast("Fehler: " + e.message, true); } - btn.disabled = false; btn.textContent = "Komplettes Setup installieren"; -} - -// ---- Hardware-Chip / Filter ---- -function renderFilters() { - $("#cb-filters").innerHTML = FILTERS.map(f => - ``).join(""); - $("#cb-filters").querySelectorAll("[data-f]").forEach(b => - b.addEventListener("click", () => { activeFilter = b.getAttribute("data-f"); renderFilters(); doSearch(); })); -} -function renderHwChip() { - const el = $("#cb-hw"); if (!el) return; - if (lastSys?.ram?.total) el.innerHTML = `${icon("cpu")}${Math.round(lastSys.ram.total / 1024 ** 3)} GB Speicher`; -} - -// ---- Profi-Suche (HuggingFace) ---- -function metricLine(fit) { return `~${fit.req_gb.toFixed(1)} GB · ~${Math.round(fit.tps)} Tok/s`; } - -// Erkennt eine direkte Modell-Angabe: ganze HuggingFace-URL ODER „owner/repo". -// So kann man Links wie https://huggingface.co/unsloth/MiniMax-M3-GGUF einfach -// reinkopieren und landet exakt auf diesem Modell (statt einer unscharfen Suche). -function parseHfRepo(s) { - s = s.trim(); - const u = s.match(/huggingface\.co\/([^\s/]+\/[^\s/?#]+)/i); - if (u) return u[1]; - if (/^[\w.-]+\/[\w.-]+$/.test(s)) return s; - return null; -} - -async function doSearch() { - const raw = $("#cb-search").value.trim(); - const repo = parseHfRepo(raw); - let q = raw; - if (activeFilter && !repo) q = q ? q + " " + activeFilter : activeFilter; - if (!q && !repo) { $("#cb-grid").innerHTML = ""; return; } - const btn = $("#cb-btn-search"); btn.disabled = true; btn.textContent = "Lade…"; - $("#cb-grid").innerHTML = `
    Suche auf HuggingFace…
    `; - try { - if (repo) { - // Direkter Treffer per Link/Repo — keine Stichwortsuche, sondern genau dieses Modell. - currentResults = [{ id: repo, author: repo.split("/")[0], downloads: 0 }]; - renderResults(currentResults); - } else { - const sort = $("#cb-sort")?.value || "downloads"; - // limit=40: auch große/seltenere Modelle (z.B. Llama-4-Scout 109B) erscheinen, - // statt von den Top-12-Trends verdrängt zu werden. Fit-Ampel sagt ehrlich, was passt. - const url = `https://huggingface.co/api/models?search=${encodeURIComponent(q)}&filter=gguf&sort=${encodeURIComponent(sort)}&direction=-1&limit=40`; - const r = await fetch(url); - currentResults = await r.json(); - renderResults(currentResults); - } - } catch (e) { $("#cb-grid").innerHTML = `
    ${esc(e.message)}
    `; } - btn.disabled = false; btn.textContent = "Suchen"; -} - -function renderResults(results) { - const grid = $("#cb-grid"); - cardFits = []; - if (!results?.length) { grid.innerHTML = `
    Keine GGUF-Modelle gefunden.
    `; return; } - grid.innerHTML = results.map((m, i) => ` -
    -
    -

    ${esc(m.id.split("/").pop())}

    -
    ${esc(m.author || "")}
    - prüfe… -
    -
    ${capChip(capLabel(m.id))}${moeChip(m.id)}
    -
    -
    - Hardware-Fit… - ⬇ ${(m.downloads || 0).toLocaleString()}
    -
    `).join(""); - grid.querySelectorAll("[data-res]").forEach(el => el.addEventListener("click", () => openResult(+el.getAttribute("data-res")))); - results.forEach((m, i) => fetchFitForCard(i, m.id)); -} - -async function fetchFitForCard(i, repo_id) { - try { - const res = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id, ctx: 8192 }) }); - const b = $("#cb-b-" + i), mt = $("#cb-m-" + i); - if (!b || !mt) return; - if (!res.files?.length) { b.className = "fit-badge"; b.style.cssText = "background:rgba(139,151,165,.14);color:var(--mut)"; b.title = GGUF_HELP; b.textContent = "kein GGUF"; mt.textContent = "—"; cardFits[i] = null; markBestResult(); return; } - let best = res.files.find(f => f.quant?.includes("Q4_K_M")) || res.files[0]; - b.className = "fit-badge " + fitCls(best.fit.level); b.textContent = best.fit.text; - mt.textContent = metricLine(best.fit) + " · " + (best.quant || "GGUF"); - cardFits[i] = best.fit.level; markBestResult(); - } catch { const b = $("#cb-b-" + i); if (b) { b.className = "fit-badge bad"; b.textContent = "Fehler"; } cardFits[i] = null; markBestResult(); } -} - -// Markiert die beste System-Wahl in den Suchergebnissen: das meistgeladene Modell -// (Liste ist nach Downloads sortiert), das noch sauber auf die Hardware passt. -function markBestResult() { - const grid = $("#cb-grid"); if (!grid) return; - let bestI = -1, bestRank = 99; - cardFits.forEach((lvl, i) => { - if (lvl == null) return; - const rank = FIT_RANK[lvl] ?? 99; - if (rank < bestRank) { bestRank = rank; bestI = i; } - }); - grid.querySelectorAll("[data-res]").forEach(el => { el.classList.remove("res-best"); el.querySelector(".cb-best-tag")?.remove(); }); - if (bestI < 0 || bestRank > 1) return; // nur hervorheben, wenn etwas Lauffähiges (perfect/marginal) dabei ist - const el = grid.querySelector(`[data-res="${bestI}"]`); - if (el) { el.classList.add("res-best"); el.insertAdjacentHTML("afterbegin", `
    ★ Beste Wahl für dein System
    `); } -} - -function showFit() { - const file = $("#cb-m-files").value; - const f = currentAnalysis?.files.find(x => x.filename === file); - if (!f) { $("#cb-m-fit").style.display = "none"; return; } - $("#cb-m-fit").style.display = "flex"; - $("#cb-m-fit-text").innerHTML = `Bedarf: ~${f.fit.req_gb.toFixed(1)} GB · ${currentAnalysis.params_b}B · ${esc(f.quant)} · ~${Math.round(f.fit.tps)} Tok/s`; - $("#cb-m-fit-badge").innerHTML = `${esc(f.fit.text)}`; - const opt = f.optimal_ctx, recEl = $("#cb-m-ctx-rec"); - if (opt) { - // Kontext-Input auto auf optimal setzen, wenn noch auf Default (8192) - const ctxInput = $("#cb-m-ctx"); - if (ctxInput && (ctxInput.value === "8192" || ctxInput.value === "")) ctxInput.value = opt; - } - if (recEl) recEl.innerHTML = opt - ? `Empfohlener Kontext für deine Hardware: ~${Math.round(opt / 1024)}kübernehmen` : ""; - const btn = $("#cb-m-download"); - if (f.fit.level === "too_tight") { btn.className = "primary warn"; btn.textContent = "Trotzdem holen (zu groß)"; } - else { btn.className = "primary"; btn.textContent = "Herunterladen & Einpflegen"; } -} -function updateLiveFit() { showFit(); } - -async function reanalyzeCtx() { - if (!currentAnalysis) return; - const ctx = parseInt($("#cb-m-ctx").value) || 8192; - const file = $("#cb-m-files").value; - try { - currentAnalysis = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id: currentAnalysis.repo, ctx }) }); - $("#cb-m-files").value = file; showFit(); - } catch {} -} - -function openModalBase(title, repo) { - $("#cb-modal").style.display = "flex"; - $("#cb-m-title").textContent = title; - $("#cb-m-repo").textContent = repo; -} - -async function openResult(i) { - const m = currentResults[i]; if (!m) return; - openModalBase(m.id.split("/").pop(), m.id); - $("#cb-m-alias").value = ""; - $("#cb-m-files").style.display = "none"; $("#cb-m-loading").style.display = "block"; $("#cb-m-download").disabled = true; - try { - const ctx = parseInt($("#cb-m-ctx").value) || 8192; - currentAnalysis = await api("/api/cookbook/analyze", { method: "POST", body: JSON.stringify({ repo_id: m.id, ctx }) }); - $("#cb-m-loading").style.display = "none"; $("#cb-m-files").style.display = "block"; - if (!currentAnalysis.files?.length) { $("#cb-m-files").innerHTML = ""; $("#cb-m-fit").style.display = "none"; return; } - $("#cb-m-files").innerHTML = currentAnalysis.files.map(f => { - const mark = f.fit.level === "perfect" ? "●" : f.fit.level === "marginal" ? "◐" : "○"; - return ``; - }).join(""); - $("#cb-m-download").disabled = false; showFit(); - } catch (e) { $("#cb-m-loading").textContent = "Fehler: " + e.message; } -} - -async function doDownload() { - const repo = $("#cb-m-repo").textContent, file = $("#cb-m-files").value; - const role = $("#cb-m-alias").value.trim().toLowerCase(), ctx = parseInt($("#cb-m-ctx").value) || 8192; - if (!repo || !file) return toast("Bitte eine GGUF-Datei wählen.", true); - const btn = $("#cb-m-download"); btn.disabled = true; btn.textContent = "Starte…"; - try { - const res = await api("/api/download", { method: "POST", body: JSON.stringify({ repo, file, hf_token: getHfToken() }) }); - await api("/api/register", { method: "POST", body: JSON.stringify({ role, model_path: res.expected_path, ctx }) }); - toast("Download gestartet — siehe Aktivität."); - $("#cb-modal").style.display = "none"; - document.querySelector(".nav-item[data-view='activity']")?.click(); - } catch (e) { toast("Fehler: " + e.message, true); } - btn.disabled = false; btn.textContent = "Herunterladen & Einpflegen"; -} - -function onSystem(sys) { lastSys = sys; renderHwChip(); } - -export default { id: "cookbook", mount, onSystem }; diff --git a/static/js/panels/guides.js b/static/js/panels/guides.js deleted file mode 100644 index e2b3c5c..0000000 --- a/static/js/panels/guides.js +++ /dev/null @@ -1,145 +0,0 @@ -// guides.js — Anfänger-Guide (v6): praktische Tutorials/Workflows + kompakte Konzepte. - -import { $ } from "../core/ui.js"; - -const TUTORIALS = [ - ["Dein erster Workflow — in 3 Schritten", ` -
      -
    1. Modell holen: Tab Cookbook → wähle deinen Anwendungsfall (z.B. „Coden") → - Komplettes Setup installieren. Der Download läuft im Tab Aktivität.
    2. -
    3. Verbinden: Tab Verbinden → Basis-URL kopieren → in deinem Tool (z.B. OpenCode) - als „OpenAI-kompatibel" einfügen. Verbindung testen bestätigt, dass es läuft.
    4. -
    5. Arbeiten: Im Tool das Modell (z.B. coder) wählen und loslegen. Das passende - Modell wird automatisch geladen — du musst nichts starten.
    6. -
    `], - ["Wie leere ich den Kontext? Machen die Tools das selbst?", ` -

    Kurz: meistens ja, automatisch. Der Kontext (das „Gedächtnis") gilt pro Unterhaltung. - Startest du im Tool einen neuen Chat / eine neue Session, ist der Kontext wieder leer — - das Tool verwaltet das Fenster für dich.

    -

    Du musst nur selten eingreifen: wird ein Gespräch sehr lang, fang einen neuen an. Wichtig: - „Grafikspeicher leeren" (Tab Server) ist etwas anderes — das wirft das Modell aus dem Speicher, - nicht den Gesprächsverlauf.

    `], - ["Was passiert, wenn der Kontext volläuft?", ` -

    Ist das Kontextfenster voll, passiert je nach Tool eins von dreien: das Modell vergisst den - Anfang (älteste Nachrichten fallen raus), das Tool kürzt automatisch, oder du bekommst - einen Fehler wie „context length exceeded".

    -

    So gehst du vor:

    -
      -
    1. Neuen Chat starten — der einfachste Reset, Kontext ist wieder leer.
    2. -
    3. Zusammenfassen lassen: „Fasse unseren Stand kurz zusammen", dann im neuen Chat einfügen.
    4. -
    5. Kontext vergrößern: Tab ModelleKonfigurieren → wir zeigen das Optimum - für deine Hardware (mehr Kontext = mehr Speicherbedarf).
    6. -
    `], - ["Was ist ein Agent — und wann brauche ich einen?", ` -

    Ein nacktes Modell schreibt nur Text zurück. Ein Agent ist ein Programm drumherum, - das das Modell handeln lässt: Dateien lesen und ändern, Befehle ausführen, im Web suchen — - Schritt für Schritt in einer Schleife, bis die Aufgabe erledigt ist.

    -

    Bild dazu: Reiner Chat = du fragst einen Experten und tippst seine Antwort selbst ab. - Agent = der Experte sitzt an deinem PC und macht es selbst — du schaust zu und bestätigst.

    -

    Wann lohnt sich was?

    -
      -
    • Fragen, Texte, Erklärungen, Brainstormingkein Agent nötig, eine Chat-Oberfläche - wie OpenWebUI reicht völlig.
    • -
    • Coden über mehrere Dateien, Befehle ausführen, recherchieren + umsetzen, automatisieren - → Agent (OpenCode, Cline, Continue).
    • -
    -

    So arbeitest du damit: Du beschreibst ein Ziel in Worten („baue Feature X", „finde den - Bug in dieser Datei"). Der Agent macht sich einen Plan und arbeitet ihn ab; heikle Schritte bestätigst - du. Lass ihn am besten in einem Projektordner mit Git arbeiten — dann ist jede Änderung - nachvollziehbar und rückgängig zu machen.

    -

    Gut zu wissen: Diese Agenten laufen lokal auf deinem PC und reden mit deinem lokalen - Modell. Du brauchst keinen Cloud-Dienst — die „empfohlenen Tools" im Tab Verbinden SIND - die Agenten.

    `], - ["Was sind Skills — und wie installiere ich sie?", ` -

    Ein Skill ist verpacktes Können: eine kurze Anleitung (oft + ein paar Hilfsskripte), - die dem Agenten beibringt, eine wiederkehrende Aufgabe jedes Mal so zu erledigen, wie du - es willst. Stell es dir wie ein Rezept oder eine Arbeitsanweisung vor, die der Agent liest, sobald sie passt.

    -

    Skill vs. MCP — nicht verwechseln: MCP gibt dem Agenten neue Werkzeuge (Aktionen, - z.B. „auf eine Datenbank zugreifen"). Skills geben ihm Know-how (wie er etwas gut macht) - mit den Werkzeugen, die er schon hat. Kurz: Werkzeuge = MCP, Anleitungen = Skills.

    -

    Wofür gut?

    -
      -
    • Einheitlichkeit: „Commit-Nachrichten immer in diesem Format", „unsere Komponenten sehen so aus".
    • -
    • Wiederholung sparen: du musst denselben Kontext nicht jedes Mal neu erklären.
    • -
    • Wissen festhalten: „so testen wir", „so wird ein Changelog geschrieben", Fach-Workflows.
    • -
    -

    In deinen Tools heißt das konkret:

    -
      -
    • OpenCode: Projektregeln in einer AGENTS.md + eigene Befehle (Commands).
    • -
    • Cline: Regeln als .clinerules + „Workflows".
    • -
    • Continue: „Rules" + „Prompts".
    • -
    • Offener Standard „Agent Skills": ein Ordner mit einer SKILL.md (Beschreibung + - Anleitung, optional Skripte) — zunehmend von mehreren Tools unterstützt.
    • -
    -

    Installieren = meist nur eine Datei/einen Ordner ablegen (kein Build, kein Neustart-Zauber):

    -
      -
    1. Pro Projekt: die Datei in den Projektordner legen (z.B. AGENTS.md, - .clinerules/) — gilt nur für dieses Projekt.
    2. -
    3. Global: in den Konfig-Ordner des Tools — gilt überall. Das Tool liest es beim nächsten Start.
    4. -
    -

    Beste Quellen: zuerst die Doku deines Tools (OpenCode/Cline/Continue) — am verlässlichsten. - Dann „awesome"-Listen auf GitHub (z.B. awesome-claude-skills, awesome-mcp-servers) und - die quelloffenen Anthropic-Skills als Vorlagen, um das Format zu lernen.

    -

    Für lokale Modelle wichtig: Skills sind nur Text-Anleitungen - und funktionieren mit jedem Modell — aber kleinere lokale Modelle befolgen komplexe Skills weniger - zuverlässig als große. Halte sie kurz, klar und konkret.

    `], - ["MCP — deinem Agenten echte Werkzeuge geben", ` -

    MCP (Model Context Protocol) ist ein einheitlicher „Stecker", über den ein Agent zusätzliche - Werkzeuge bekommt: das Web durchsuchen, Dateien lesen, eine Datenbank abfragen, GitHub bedienen … - Ohne MCP kann das Modell nur reden; mit MCP kann es etwas tun.

    -

    So nutzt du es: Du fügst in deinem Agenten einen „MCP-Server" hinzu (eine kleine Zeile in der - Konfiguration je Werkzeug). Danach entscheidet der Agent selbst, wann er das Werkzeug einsetzt.

    -
      -
    • Unterstützt: Cline und Continue haben das beste MCP-Ökosystem; viele fertige Server gibt es - auf GitHub (Suchbegriff awesome-mcp-servers).
    • -
    • Faustregel: Fehlt dem Agenten eine Fähigkeit (z.B. Web-Zugriff) → MCP. Soll er etwas - nur besser/einheitlicher machen → Skill.
    • -
    `], - ["Auto-Swap — wie das Laden funktioniert", ` -

    llama-swap lädt automatisch das Modell, das gerade angefragt wird, und entlädt es nach einer - Leerlaufzeit wieder (oder wenn du ein anderes anfragst). Es läuft immer nur eines gleichzeitig.

    -

    Heißt für dich: Du richtest beliebig viele Modelle ein und wählst im Tool einfach das passende — - das Laden/Entladen passiert im Hintergrund, ohne dass du etwas tun musst.

    `], - ["Die empfohlenen Tools optimal nutzen", ` -
      -
    • OpenCode: Autonomes Coden im Terminal/Editor — ideal für „bau mir Feature X".
    • -
    • Cline: Dasselbe als VS-Code-Erweiterung, mit Bestätigung vor jeder Aktion.
    • -
    • OpenWebUI: Klassische Chat-Oberfläche für Fragen/Texte.
    • -
    -

    Tipp: starkes Modell (coder) für schwere Aufgaben, schnelles - (coder-fast) für simple — du wählst es einfach im Tool aus.

    `], -]; - -const CONCEPTS = [ - ["LLM / Modell", "Die KI selbst — versteht und schreibt Text (wie ChatGPT, nur lokal auf deinem PC). Ein Modell ist eine konkrete Datei davon, oft auf etwas spezialisiert (Code, Bilder, Chat)."], - ["GGUF", "Das lokal lauffähige Dateiformat. Unsere Engine lädt nur GGUF — Repos ohne GGUF kann sie nicht nutzen."], - ["Quantisierung (Q4_K_M)", "Verkleinert ein Modell (16-Bit → 4-Bit), damit es in den Speicher passt — bei kaum Qualitätsverlust. Q4_K_M ist der beliebte Sweetspot."], - ["Kontextfenster", "Das Kurzzeitgedächtnis: wie viel Text sich das Modell gleichzeitig merkt. Größer = mehr Überblick, mehr Speicher, etwas langsamer."], - ["Auto-Swap", "llama-swap lädt/entlädt Modelle automatisch — immer nur eins gleichzeitig. Du managst nichts."], - ["Agent", "Ein Programm (OpenCode, Cline, Continue), das das Modell handeln lässt — Dateien lesen/ändern, Befehle ausführen, planen, im Loop bis die Aufgabe erledigt ist. Reiner Chat schreibt nur Text zurück."], - ["MCP", "Model Context Protocol — der Anschluss, über den ein Agent zusätzliche Werkzeuge bekommt (Web, Dateien, Datenbank). MCP = Werkzeuge."], - ["Skills", "Verpackte Anleitung für wiederkehrende Aufgaben — der Agent liest sie und macht es jedes Mal wie du willst. Skills = Anleitungen (das Gegenstück zu MCP-Werkzeugen)."], - ["Rules / Commands", "Die konkrete Form von Skills je Tool: AGENTS.md (OpenCode), .clinerules (Cline), Rules/Prompts (Continue) — eine Datei im Projekt- oder Konfig-Ordner."], -]; - -function mount() { - const c = $(".view[data-view='guides']"); if (!c) return; - c.innerHTML = ` -
    -

    Guide für Einsteiger

    -
    Praktische Workflows + die Local-LLM-Welt in Klartext — ohne Vorwissen.
    - -

    Tutorials & Workflows

    -
    - ${TUTORIALS.map(([t, b], i) => ` -
    ${t} -
    ${b}
    `).join("")} -
    - -

    Konzepte kurz erklärt

    -
    - ${CONCEPTS.map(([term, text]) => `

    ${term}

    ${text}

    `).join("")} -
    `; -} - -export default { id: "guides", mount }; diff --git a/static/js/panels/jobs.js b/static/js/panels/jobs.js deleted file mode 100644 index 39a8001..0000000 --- a/static/js/panels/jobs.js +++ /dev/null @@ -1,155 +0,0 @@ -// jobs.js — Aktivität (v3): Live-System-Metriken + Hintergrund-Jobs mit Log. -// Exportiert track(id), damit andere Panels einen Job auto-aufklappen. - -import { api } from "../core/api.js"; -import { $, esc, fmtBytes, toast } from "../core/ui.js"; - -const tracked = new Set(); -let JOBS = []; -let SYS = null; - -export function track(id) { tracked.add(id); renderJobs(); } - -const hist = { cpu: [], ram: [], gpu: [] }; -const MAX_HIST = 60; - -function statusBadge(s) { return s === "done" ? 'fertig' : s === "failed" ? 'fehler' : s === "canceled" ? 'abgebrochen' : 'läuft…'; } -function dotClass(s) { return s === "done" ? "on" : (s === "failed" || s === "canceled") ? "" : "load"; } - -// Fortschritt in %: bevorzugt der exakte Wert vom Backend (Download-Datei-Polling), -// sonst Fallback auf eine %-Angabe in der letzten Log-Zeile. -function jobPct(j) { - if (j.state !== "running") return null; - if (typeof j.progress === "number") return Math.min(100, j.progress); - const last = (j.log || [])[j.log.length - 1] || ""; - const m = last.match(/(\d+(?:\.\d+)?)\s*%/); - return m ? Math.min(100, parseFloat(m[1])) : null; -} - -// Restzeit menschlich: "noch ~2 min" / "noch ~45 s". -function fmtEta(s) { - if (s == null || s < 0) return ""; - if (s >= 90) return `noch ~${Math.round(s / 60)} min`; - return `noch ~${Math.max(1, Math.round(s))} s`; -} - -async function cancelJob(id) { - try { await api(`/api/jobs/${id}/cancel`, { method: "POST" }); toast("Abbruch angefordert…"); renderJobs(); } - catch (e) { toast(e.message, true); } -} - -async function deleteJob(id) { - try { await api(`/api/jobs/${id}`, { method: "DELETE" }); renderJobs(); } - catch (e) { toast(e.message, true); } -} - -async function clearJobs() { - try { const r = await api("/api/jobs/clear", { method: "POST" }); toast(`${r.removed} Einträge entfernt.`); renderJobs(); } - catch (e) { toast(e.message, true); } -} - -function tile(label, value, sub) { - return `
    ${label}
    ${value}
    ${sub}
    `; -} -function meter(label, pct) { - const p = Math.max(0, Math.min(100, pct || 0)); - const cls = p >= 90 ? "bad" : p >= 75 ? "warn" : ""; - return `
    ${label}${Math.round(p)} %
    `; -} -function spark(arr, varName) { - return '
    ' + - arr.map(v => `
    `).join("") + "
    "; -} - -function gpuPct() { - const g = SYS?.gpu; - if (g && (g.vram.total + g.gtt.total) > 0) return ((g.vram.used + g.gtt.used) / (g.vram.total + g.gtt.total)) * 100; - return 0; -} - -function renderSys() { - if (!SYS) return; - hist.cpu.push(SYS.cpu.percent); hist.ram.push(SYS.ram.percent); hist.gpu.push(gpuPct()); - for (const k of ["cpu", "ram", "gpu"]) if (hist[k].length > MAX_HIST) hist[k].shift(); - - const k = $("#act-kpis"); - if (k) k.innerHTML = - tile("Prozessor (CPU)", `${Math.round(SYS.cpu.percent)} %`, SYS.cpu.temp != null ? `${Math.round(SYS.cpu.temp)}° CPU-Temp` : "Auslastung") + - tile("Arbeitsspeicher", `${Math.round(SYS.ram.percent)} %`, `${fmtBytes(SYS.ram.used)} / ${fmtBytes(SYS.ram.total)}`) + - tile("Grafikspeicher", `${Math.round(gpuPct())} %`, SYS.gpu_temp != null ? `${Math.round(SYS.gpu_temp)}° GPU-Temp` : "VRAM + GTT"); - - const g = SYS.gpu; - const gpuStr = g && (g.vram.total + g.gtt.total) > 0 ? `${fmtBytes(g.vram.used + g.gtt.used)} / ${fmtBytes(g.vram.total + g.gtt.total)}` : "–"; - const s = $("#act-sys"); - if (s) s.innerHTML = ` -

    System-Metriken (Bosgame)

    -
    Live-Auslastung deines Mini-PCs, alle 0,5 Sekunden.
    -
    -
    Arbeitsspeicher (RAM)${fmtBytes(SYS.ram.used)} / ${fmtBytes(SYS.ram.total)}
    -
    Grafikspeicher (VRAM + GTT)${gpuStr}
    -
    Speicherplatz (Disk)${Math.round(SYS.disk.percent)} % belegt
    -
    Temperatur (GPU / CPU)${SYS.gpu_temp != null ? Math.round(SYS.gpu_temp) + "°" : "–"} / ${SYS.cpu.temp != null ? Math.round(SYS.cpu.temp) + "°" : "–"}
    -
    -
    -
    CPU-Verlauf
    ${spark(hist.cpu, "--accent")}
    -
    RAM-Verlauf
    ${spark(hist.ram, "--purple")}
    -
    VRAM-Verlauf
    ${spark(hist.gpu, "--on")}
    -
    `; -} - -function mount() { - $("#act-head").innerHTML = `
    -

    Aktivität

    -
    Live-Auslastung und laufende Aufgaben (Downloads, Updates) mit Protokoll.
    `; - - $("#v-activity").innerHTML = ` -

    Hintergrund-Aufgaben

    -
    -
    Downloads & Updates erscheinen hier mit Live-Protokoll — zum Aufklappen klicken.
    -
    -
    Gerade nichts los.
    Alles ruhig — keine laufenden Aufgaben.
    `; - - $("#v-activity").addEventListener("click", e => { - if (e.target.closest("#jobs-clear")) { clearJobs(); return; } - const cancel = e.target.closest("[data-cancel]"); - if (cancel) { e.stopPropagation(); cancelJob(cancel.getAttribute("data-cancel")); return; } - const del = e.target.closest("[data-del]"); - if (del) { e.stopPropagation(); deleteJob(del.getAttribute("data-del")); return; } - const h = e.target.closest(".job-h"); if (!h) return; - const id = h.getAttribute("data-id"); - tracked.has(id) ? tracked.delete(id) : tracked.add(id); - renderJobs(); - }); - if (SYS) renderSys(); -} - -function renderJobs() { - const c = $("#jobs"); if (!c) return; - $("#jobs-empty").style.display = JOBS.length ? "none" : "flex"; - const failed = JOBS.filter(j => j.state === "failed").length; - const finished = JOBS.filter(j => j.state !== "running" && j.state !== "queued").length; - $("#job-count").textContent = JOBS.length ? (failed ? failed + " Fehler" : JOBS.length + " gesamt") : ""; - const clearBtn = $("#jobs-clear"); if (clearBtn) clearBtn.style.display = finished ? "" : "none"; - c.innerHTML = JOBS.map(j => { - const log = tracked.has(j.id) ? `
    ${esc((j.log || []).join("\n"))}
    ` : ""; - const p = jobPct(j); - const prog = p != null ? `
    ` : ""; - // bei laufenden Downloads: Prozent + Restzeit + Rate - const eta = j.state === "running" ? fmtEta(j.eta_s) : ""; - const rate = j.state === "running" && j.rate_bps ? fmtBytes(j.rate_bps) + "/s" : ""; - const extra = [eta, rate].filter(Boolean).join(" · "); - const pctTxt = p != null - ? `${Math.round(p)} %${extra ? ` · ${extra}` : ""}` - : ""; - const cancelBtn = j.state === "running" - ? `` - : ``; - return `
    - ${esc(j.label)}${pctTxt}${cancelBtn}${statusBadge(j.state)}
    ${prog}${log}
    `; - }).join(""); -} - -function onJobs(jobs) { JOBS = jobs || []; renderJobs(); } -function onSystem(sys) { SYS = sys; if ($("#act-sys")) renderSys(); } - -export default { id: "jobs", mount, onJobs, onSystem }; diff --git a/static/js/panels/models.js b/static/js/panels/models.js deleted file mode 100644 index a55cb8b..0000000 --- a/static/js/panels/models.js +++ /dev/null @@ -1,268 +0,0 @@ -// models.js — "Modelle"-Ansicht (v3): Schnelltest-Chat, Modell-Tabelle, Kontext-Konfiguration. - -import { api } from "../core/api.js"; -import { $, badge, esc, toast, confirmModal, infoDot } from "../core/ui.js"; - -let JOBS_CACHE = []; - -const CTX_HELP = "Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr (längere Dateien/Chats), braucht aber mehr Speicher und wird etwas langsamer."; -const ROLE_HELP = "Die Rolle ist ein zusätzlicher, sprechender Name (z.B. coder). Du kannst in deinen Tools entweder den echten Modellnamen ODER die Rolle angeben — beides führt zum selben Modell."; - -// Kanonische Rollen für die Schnellwahl (frei tippbar bleibt möglich). -const ROLES = [ - { id: "coder", label: "Coder", desc: "Programmieren & Code" }, - { id: "vision", label: "Vision", desc: "Bilder verstehen" }, - { id: "scout", label: "Scout", desc: "Schneller Allrounder" }, - { id: "reviewer", label: "Reviewer", desc: "Code/Texte prüfen" }, - { id: "manager", label: "Manager", desc: "Planen & koordinieren" }, -]; - -let ALL = []; -function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); } - -// Badge mit Download-Fortschritt aus Backend-State (download_progress vom status endpoint) -function stateBadge(m) { - return badge(m.state, m.download_progress); -} - -function roleTag(role) { - return role ? `${esc(role)}` : ""; -} -// Zeigt die API-Namen (echter Name + Rollen-Alias) mit Kopier-Knopf — das, was man im Tool angibt. -function apiIdLine(m) { - const ids = (m.api_ids && m.api_ids.length ? m.api_ids : [m.name]); - return `
    - API-Name: - ${ids.map(id => `${esc(id)}`).join('·')} -
    `; -} - -function capTags(caps) { - if (!caps?.length) return "–"; - return caps.map(c => - c === "Code" ? `Code` - : c === "Bild" ? `Bild` - : `Text`).join(" "); -} -function details(meta) { - if (!meta) return "–"; - const q = meta.quant || "?"; - const c = meta.ctx ? Math.round(meta.ctx / 1024) + "K" : "?"; - const s = meta.size_bytes ? (meta.size_bytes / 1024 ** 3).toFixed(1) + " GB" : "?"; - return `${esc(q)} · ${c} · ${s}`; -} - -function mount() { - $("#m-head").innerHTML = `
    -

    Modelle

    -
    Deine konfigurierten Modelle — testen, Kontext anpassen oder aus dem Speicher werfen.
    `; - - $("#m-chat").innerHTML = ` -

    Schnelltest

    -
    Schreib eine Nachricht — das gewählte Modell wird automatisch geweckt.
    - - - - - - `; - - $("#m-table").innerHTML = ` -

    Modelle & Ports

    -
    Modelle laden automatisch, sobald eine Anfrage kommt — du musst nichts manuell starten.
    - - - -
    ModellKannDetailsStatusPortAktionen
    - - - - - `; - - $("#chat-btn").addEventListener("click", sendChat); - $("#cfg-close").addEventListener("click", () => $("#cfg-modal").style.display = "none"); - $("#cfg-modal").addEventListener("click", e => { if (e.target.id === "cfg-modal") $("#cfg-modal").style.display = "none"; }); - $("#cfg-save").addEventListener("click", saveConfig); - $("#role-close").addEventListener("click", () => $("#role-modal").style.display = "none"); - $("#role-modal").addEventListener("click", e => { if (e.target.id === "role-modal") $("#role-modal").style.display = "none"; }); - $("#role-save").addEventListener("click", () => saveRole($("#role-custom").value.trim().toLowerCase())); - $("#role-clear").addEventListener("click", () => saveRole("")); - - // Event-Delegation: ein einziger Listener für alle Tabellen-Aktionen - $("#m-table").addEventListener("click", e => { - const btn = e.target.closest("[data-action]"); - if (!btn) return; - const name = btn.getAttribute("data-name"), action = btn.getAttribute("data-action"); - if (!name) return; - if (action === "cfg") openConfig(name); - else if (action === "role") openRole(name); - else if (action === "unload") unloadOne(name); - else if (action === "del") deleteModel(name); - else if (action === "copy") { navigator.clipboard?.writeText(name); toast("Kopiert: " + name); } - else if (action === "assign") { - toast("Lade im Cookbook ein Modell und setze den Alias auf: " + name); - document.querySelector(".nav-item[data-view='cookbook']")?.click(); - } - }); -} - -let roleTarget = null; -function openRole(name) { - roleTarget = name; - const m = ALL.find(x => x.name === name); - $("#role-model-name").textContent = name; - $("#role-custom").value = ""; - $("#role-chips").innerHTML = ROLES.map(r => - ``).join(""); - $("#role-chips").querySelectorAll("[data-rolepick]").forEach(b => - b.addEventListener("click", () => saveRole(b.getAttribute("data-rolepick")))); - $("#role-modal").style.display = "flex"; -} -async function saveRole(role) { - if (!roleTarget) return; - try { - await api("/api/set_role", { method: "POST", body: JSON.stringify({ alias: roleTarget, role }) }); - toast(role ? `Rolle gesetzt: ${role}` : "Rolle entfernt."); - $("#role-modal").style.display = "none"; - refreshSoon(); - } catch (e) { toast(e.message, true); } -} - -function renderTable() { - const tb = $("#models"); if (!tb) return; - $("#models-empty").style.display = ALL.length ? "none" : "flex"; - $("#m-count").textContent = ALL.length ? ALL.length + " konfiguriert" : ""; - - const sel = $("#chat-model"), cur = sel.value; sel.innerHTML = ""; - tb.innerHTML = ALL.map(m => { - if (m.incomplete) { - return ` - ${esc(m.name)}
    kein Modell hinterlegt
    - — - leere Rolle - leer - — - - - - `; - } - const fn = m.meta?.filename ? `
    ${esc(m.meta.filename)}
    ` : ""; - return ` - -
    ${esc(m.name)}${roleTag(m.role)}
    - ${fn}${apiIdLine(m)} - ${capTags(m.meta?.caps)} - ${details(m.meta)} - ${stateBadge(m)} - ${m.port ?? "auto"} - - - - - - `; - }).join(""); - for (const m of ALL) if (!m.incomplete) sel.insertAdjacentHTML("beforeend", ``); - if (cur) sel.value = cur; -} - -function onStatus(s) { - ALL = s?.models || []; - renderTable(); -} - -function onJobs(jobs) { - JOBS_CACHE = jobs || []; - // Nur neu rendern wenn sich der Download-Zustand geändert hat (minimiert DOM-Flicker) - const hasDownload = JOBS_CACHE.some(j => (j.state === "running" || j.state === "queued") && /^download\b/i.test(j.label)); - if (hasDownload) renderTable(); -} - -async function deleteModel(m) { - const ok = await confirmModal({ - title: `„${m}" wirklich löschen?`, - body: "Das Modell wird aus der Konfiguration ausgetragen und seine Dateien werden von der Festplatte gelöscht, um Speicher freizugeben. Das lässt sich nicht rückgängig machen — später brauchst du dafür einen neuen Download.", - confirmLabel: "Endgültig löschen", danger: true }); - if (!ok) return; - try { - const r = await api("/api/delete_model", { method: "POST", body: JSON.stringify({ alias: m }) }); - toast(r.note || ("Gelöscht: " + m)); - setTimeout(refreshSoon, 600); - } catch (e) { toast(e.message, true); } -} - -function openConfig(alias) { - const m = ALL.find(x => x.name === alias); if (!m) return; - $("#cfg-model-name").textContent = m.name; - $("#cfg-ctx").value = m.meta?.ctx || 8192; - const opt = m.meta?.optimal_ctx, rec = $("#cfg-rec"); - if (opt) { - const cur = m.meta?.ctx || 0, peak = m.meta?.peak_ram_gb, peakOpt = m.meta?.peak_ram_optimal_gb; - $("#cfg-rec-note").innerHTML = - `Optimal ~${Math.round(opt / 1024)}k → ~${peakOpt} GB Spitzenbedarf.
    ` + - `Aktuell ~${Math.round(cur / 1024)}k → ~${peak} GB.`; - $("#cfg-rec-apply").onclick = () => { $("#cfg-ctx").value = opt; }; - rec.style.display = "flex"; - } else { rec.style.display = "none"; } - $("#cfg-modal").style.display = "flex"; -} -async function saveConfig() { - const alias = $("#cfg-model-name").textContent, ctx = parseInt($("#cfg-ctx").value) || 8192; - $("#cfg-save").disabled = true; - try { await api("/api/update_model", { method: "POST", body: JSON.stringify({ alias, ctx }) }); toast("Gespeichert — aktiv beim nächsten Modell-Start."); $("#cfg-modal").style.display = "none"; refreshSoon(); } - catch (e) { toast(e.message, true); } - $("#cfg-save").disabled = false; -} - -async function unloadOne(m) { - if (!await confirmModal({ title: `„${m}" entladen?`, body: "Das Modell wird aus dem Grafikspeicher geworfen und lädt beim nächsten Aufruf automatisch neu.", confirmLabel: "Entladen" })) return; - try { await api("/api/unload?model=" + encodeURIComponent(m), { method: "POST" }); toast("Entladen: " + m); setTimeout(refreshSoon, 600); } - catch (e) { toast(e.message, true); } -} - -async function sendChat() { - const model = $("#chat-model").value, message = $("#chat-msg").value.trim(); - if (!model) return toast("Kein Modell vorhanden.", true); - if (!message) return; - const btn = $("#chat-btn"); btn.disabled = true; btn.textContent = "…"; - const box = $("#chat-reply"); box.style.display = "block"; box.textContent = "(wecke Modell, kann beim Laden kurz dauern…)"; - try { const r = await api("/api/chat", { method: "POST", body: JSON.stringify({ model, message }) }); box.textContent = r.reply; } - catch (e) { box.textContent = "Fehler: " + e.message; } - btn.disabled = false; btn.textContent = "Senden"; refreshSoon(); -} - -export default { id: "models", mount, onStatus, onJobs }; diff --git a/static/js/panels/news.js b/static/js/panels/news.js deleted file mode 100644 index f9d90aa..0000000 --- a/static/js/panels/news.js +++ /dev/null @@ -1,79 +0,0 @@ -// news.js — News-Board (v6): Magazin-Layout + „Für dich: Upgrades". - -import { api, getHfToken } from "../core/api.js"; -import { $, esc, ago, toast } from "../core/ui.js"; - -let UPS = []; -const fitCls = l => l === "perfect" ? "ok" : l === "marginal" ? "warn" : "bad"; - -function mount() { - $(".view[data-view='news']").innerHTML = ` -
    -

    News

    -
    Aktuelles aus der Local-LLM-Welt — aus kuratierten Quellen (HuggingFace, r/LocalLLaMA, Latent Space, Ahead of AI …).
    - -
    -
    -
    Lade News…
    `; - $("#news-refresh").addEventListener("click", () => { load(); loadUpgrades(); }); - load(); - loadUpgrades(); -} - -// ---- Upgrade-Vorschläge ---- -function upRow(u, i) { - return `
    -
    -
    - ${esc(u.name)} - ${esc(u.fit.text)}
    -
    Ersetzt dein „${esc(u.old)}". ${esc(u.why)}
    -
    - -
    `; -} -async function loadUpgrades() { - const el = $("#upgrades"); if (!el) return; - try { - UPS = (await api("/api/cookbook/upgrades")).upgrades || []; - if (!UPS.length) { el.innerHTML = ""; return; } - el.innerHTML = `
    -

    Für dich: Upgrades

    ${UPS.length}
    -
    Neuere Modelle, die ein installiertes ersetzen können — mit Begründung.
    - ${UPS.map(upRow).join("")}
    `; - el.querySelectorAll("[data-up]").forEach(b => b.addEventListener("click", () => installUpgrade(+b.getAttribute("data-up"), b))); - } catch { el.innerHTML = ""; } -} -async function installUpgrade(i, btn) { - const u = UPS[i]; if (!u) return; - btn.disabled = true; btn.textContent = "Starte…"; - try { - await api("/api/cookbook/install-model", { method: "POST", body: JSON.stringify({ - repo: u.repo, role: u.role, params_b: u.params_b, quant: u.quant, hf_token: getHfToken() }) }); - toast(`„${u.name}" wird installiert — ersetzt „${u.old}". Siehe Aktivität.`); - document.querySelector(".nav-item[data-view='activity']")?.click(); - } catch (e) { toast("Fehler: " + e.message, true); btn.disabled = false; btn.textContent = "Installieren"; } -} - -// ---- News (Magazin-Layout) ---- -function newsCard(n, featured) { - const date = n.ts ? ago(n.ts) : ""; - const rel = n.relevant ? `für dich` : ""; - return ` -
    ${esc(n.source)}${rel}${esc(date)}
    -
    ${esc(n.title)}
    -
    `; -} -async function load() { - const el = $("#news-list"); if (!el) return; - el.innerHTML = `
    Lade News… (erstes Laden kann kurz dauern)
    `; - try { - const items = (await api("/api/news")).items || []; - el.innerHTML = items.length - ? `
    ${items.map((n, i) => newsCard(n, i === 0)).join("")}
    ` - : `
    Keine News geladen
    -
    Die Quellen sind gerade nicht erreichbar — später nochmal probieren.
    `; - } catch (e) { el.innerHTML = `
    ${esc(e.message)}
    `; } -} - -export default { id: "news", mount }; diff --git a/static/js/panels/overview.js b/static/js/panels/overview.js deleted file mode 100644 index fe8946f..0000000 --- a/static/js/panels/overview.js +++ /dev/null @@ -1,165 +0,0 @@ -// overview.js — Dashboard (v3): Klartext-Urteil, Metrik-Kacheln, System-Gesundheit, -// geführter Schnellstart, "Dein Stack". Speist sich aus /api/status + /api/system/stream. - -import { api } from "../core/api.js"; -import { $, esc, icon, toast, fmtBytes, confirmModal } from "../core/ui.js"; - -let S = null; // letzter Status -let SYS = null; // letzte System-Metriken - -const RUNNING = new Set(["running", "ready", "loading", "starting"]); - -function models() { return S?.models || []; } -function activeModel() { return models().find(m => RUNNING.has(m.state)); } - -// ---- Aktionen (geführt) ---- -function go(view) { document.querySelector(`.nav-item[data-view="${view}"]`)?.click(); } -async function freeMemory() { - const ok = await confirmModal({ - title: "Speicher freigeben?", - body: "Alle aktuell geladenen Modelle werden aus dem Grafikspeicher geworfen. " + - "Beim nächsten Aufruf lädt das jeweilige Modell einfach neu — es geht nichts verloren.", - confirmLabel: "Speicher freigeben", - }); - if (!ok) return; - try { await api("/api/unload", { method: "POST" }); toast("Speicher freigegeben."); setTimeout(() => document.dispatchEvent(new Event("mc:refresh")), 600); } - catch (e) { toast(e.message, true); } -} -window.mcOv = { go, freeMemory }; - -// ---- Hero (menschliches Urteil) ---- -function renderHero() { - let title = "Verbinde…", sub = ""; - if (S) { - const total = models().length, act = activeModel(); - if (!S.swap_ok) { title = "LLM-Engine offline"; sub = "Der llama-swap-Dienst antwortet gerade nicht."; } - else if (SYS && SYS.ram.percent >= 90) { title = "Achtung: Speicher wird knapp."; sub = `Arbeitsspeicher bei ${Math.round(SYS.ram.percent)} % — eventuell Speicher freigeben.`; } - else { - title = "Alles läuft rund."; - sub = `${total} ${total === 1 ? "Modell" : "Modelle"} bereit · ${act ? `„${act.name}" geladen` : "keins geladen"} · keine Warnungen.`; - } - } - $("#ov-hero").innerHTML = `
    -

    ${esc(title)}

    ${esc(sub)}
    `; -} - -// ---- Metrik-Kacheln ---- -function tile(label, valueHtml, sub, vCls = "", sCls = "") { - return `
    ${label}
    -
    ${valueHtml}
    ${sub}
    `; -} -function tempWord(t) { return t == null ? "" : t < 55 ? "kühl & gesund" : t < 72 ? "normal" : "läuft heiß"; } -function renderTiles() { - const act = activeModel(); - const swap = S?.swap_ok; - let engine = tile("LLM-Engine", swap ? "Online" : "Offline", "llama-swap", swap ? "ok" : "bad"); - let model = tile("Aktives Modell", act ? esc(act.name) : "Keins", - act ? "im Grafikspeicher" : "nichts geladen"); - let temp, mem; - if (SYS) { - const t = SYS.gpu_temp ?? SYS.cpu.temp; - temp = tile("GPU-Temperatur", t != null ? `${Math.round(t)}°` : "–", tempWord(t), - "", t != null && t < 72 ? "ok" : ""); - const free = SYS.ram.total - SYS.ram.used; - mem = tile("Freier Speicher", `${(free / 1024 ** 3).toFixed(0)} GB`, - `von ${(SYS.ram.total / 1024 ** 3).toFixed(0)} GB`); - } else { - temp = tile("GPU-Temperatur", "–", "messe…"); - mem = tile("Freier Speicher", "–", "messe…"); - } - $("#ov-tiles").innerHTML = engine + model + temp + mem; -} - -// ---- System-Gesundheit ---- -function meter(label, pct) { - const p = Math.max(0, Math.min(100, pct || 0)); - const cls = p >= 90 ? "bad" : p >= 75 ? "warn" : ""; - return `
    ${label} - ${Math.round(p)} %
    -
    `; -} -function renderHealth() { - const head = `

    System-Gesundheit

    -
    So ausgelastet ist dein Mini-PC gerade.
    `; - if (!SYS) { $("#ov-health").innerHTML = head + `
    Warte auf Messwerte…
    `; return; } - let gpuPct = 0; - const g = SYS.gpu; - if (g && (g.vram.total + g.gtt.total) > 0) - gpuPct = ((g.vram.used + g.gtt.used) / (g.vram.total + g.gtt.total)) * 100; - $("#ov-health").innerHTML = head + - meter("Prozessor (CPU)", SYS.cpu.percent) + - meter("Arbeitsspeicher (RAM)", SYS.ram.percent) + - meter("Grafikspeicher (VRAM)", gpuPct); -} - -// ---- Schnellstart ---- -function qa(ic, tone, title, sub, onclick) { - return ``; -} -function renderQuickstart() { - $("#ov-quickstart").innerHTML = - `

    Schnellstart

    -
    Die häufigsten Aufgaben — ein Klick.
    ` + - qa("search", "teal", "Modell finden", "Passend zu deiner Hardware", "window.mcOv.go('cookbook')") + - qa("refresh", "amber", "Speicher freigeben", "Modelle entladen · fragt vorher nach", "window.mcOv.freeMemory()") + - qa("file", "blue", "Logs ansehen", "Live mitlesen, was läuft", "window.mcOv.go('server')"); -} - -// ---- Dein Stack ---- -function capTag(caps) { - if (!caps) return ""; - if (caps.includes("Bild")) return `Bild`; - if (caps.includes("Code")) return `Code`; - return `Text`; -} -function stackRow(m) { - const downloading = m.state === "downloading"; - const on = RUNNING.has(m.state); - const dot = (m.state === "loading" || m.state === "starting" || downloading) ? "load" : on ? "on" : ""; - let status; - if (downloading) { - const pct = m.download_progress != null ? ` ${Math.round(m.download_progress)}%` : ""; - status = `↓ Download${pct}`; - } else { - status = on ? (m.state === "loading" ? "lädt…" : "geladen") : "bereit"; - } - const file = m.meta?.filename ? `
    ${esc(m.meta.filename)}
    ` : ""; - return `
    -
    -
    ${esc(m.name)}${capTag(m.meta?.caps)}
    ${file}
    - ${esc(status)}
    `; -} - -async function renderNews() { - const el = $("#ov-news"); if (!el) return; - const head = `

    Top-News

    Alle →
    `; - el.innerHTML = head + `
    Lade…
    `; - try { - const items = ((await api("/api/news")).items || []).slice(0, 4); - el.innerHTML = head + (items.length - ? `
    ${items.map(n => ` -
    ${esc(n.title)}
    -
    ${esc(n.source)}
    `).join("")}
    ` - : `
    Keine News.
    `); - } catch { el.innerHTML = head + `
    News gerade nicht ladbar.
    `; } -} -function renderStack() { - const ms = models(); - $("#ov-stack").innerHTML = - `

    Dein Stack

    ${ms.length ? ms.length + " konfiguriert" : ""}
    ` + - (ms.length - ? `
    ${ms.map(stackRow).join("")}
    ` - : `
    Noch keine Modelle
    -
    Hol dir unter „Cookbook" ein Modell, das auf deine Hardware passt.
    `); -} - -function renderAll() { renderHero(); renderTiles(); renderHealth(); renderQuickstart(); renderStack(); } - -function mount() { renderQuickstart(); renderAll(); renderNews(); } -function onStatus(s) { S = s; renderHero(); renderTiles(); renderStack(); } -function onSystem(sys) { SYS = sys; renderHero(); renderTiles(); renderHealth(); } - -export default { id: "overview", mount, onStatus, onSystem }; diff --git a/static/js/panels/server.js b/static/js/panels/server.js deleted file mode 100644 index 8cc819c..0000000 --- a/static/js/panels/server.js +++ /dev/null @@ -1,242 +0,0 @@ -// server.js — Server & Wartung (v3): Dienste, OS-Updates, Reboot, Live-Konsole. -// Selbsterklärende Aktionszeilen + heikle Aktionen mit Klartext-Bestätigung. - -import { api, getToken } from "../core/api.js"; -import { $, esc, toast, icon, confirmModal, promptModal } from "../core/ui.js"; -import { track } from "./jobs.js"; - -function refreshSoon() { document.dispatchEvent(new Event("mc:refresh")); } - -function row(id, ic, tone, title, desc) { - return ``; -} - -function mount() { - $("#wartung").outerHTML = `
    -
    -

    Server & Wartung

    -
    Dienste steuern, Updates einspielen und live mitlesen — ganz ohne SSH oder Terminal.
    -
    - -
    -

    LLM-Engine (llama-swap)

    - prüfe…
    -
    Das Herzstück: llama-swap lädt bei jeder Anfrage automatisch das passende Modell und entlädt die anderen — es läuft immer nur eines gleichzeitig.
    -
    -
    Adresse (für deine Tools)
    -
    Aktuell geladen
    -
    Konfigurierte Modelle
    -
    -
    Wie ansteuern? In deinem Tool (Zed/OpenCode) die obige Adresse als OpenAI-Basis-URL eintragen und einen Modellnamen oder eine Rolle angeben (siehe „Verbinden"). Eine einzige Anfrage genügt — das Modell wird automatisch geweckt.
    -
    -
    - -
    -

    Dienste & Applikation

    -
    Diese Aktionen sind harmlos und passwortlos — es geht nichts dabei verloren.
    - ${row("w-restart-swap", "refresh", "teal", "LLM-Engine neustarten", "Startet llama-swap neu. Geladene Modelle werden kurz entladen (~5 Sek), laden danach automatisch wieder.")} - ${row("w-restart-mc", "refresh", "blue", "Dashboard neustarten", "Startet diese Oberfläche neu. Sie trennt kurz und verbindet automatisch wieder.")} - ${row("w-self-update", "download", "teal", "Mission Control aktualisieren", "Holt die neueste Dashboard-Version aus Gitea und startet automatisch neu (~10 Sek).")} - ${row("w-update", "bolt", "blue", "LLM-Engine aktualisieren", "Lädt die neueste llama.cpp-Version (ROCm) für deine GPU und installiert sie.")} - ${row("w-unload", "database", "amber", "Grafikspeicher leeren", "Wirft alle aktuell geladenen Modelle aus dem VRAM. Fragt vorher nach.")} -
    - -
    -

    Betriebssystem (Bosgame)

    -
    Tiefe Eingriffe — das Dashboard fragt einmalig nach deinem sudo-Passwort.
    - ${row("w-os-update", "refresh", "amber", "OS-Updates installieren", "Führt apt update & upgrade aus. Kann ein paar Minuten dauern — Fortschritt in der Aktivität.")} - ${row("w-reboot", "bolt", "red", "Server neustarten (Reboot)", "Bootet den ganzen Bosgame neu. Alles ist für ~1 Minute offline, auch dieses Dashboard.")} -
    - - - -
    -

    Live-Konsole

    - -
    -
    Live mitlesen, was der Dienst gerade tut.
    -
    Verbinde…
    -
    -
    `; - - $("#w-restart-swap").addEventListener("click", () => restartService("llama-swap", - "LLM-Engine neustarten?", "Die Engine startet neu. Geladene Modelle werden kurz entladen (~5 Sekunden), laden danach automatisch wieder.")); - $("#w-restart-mc").addEventListener("click", () => restartService("mission-control", - "Dashboard neustarten?", "Diese Oberfläche trennt sich kurz und verbindet automatisch wieder. Laufende Downloads laufen weiter.")); - $("#w-self-update").addEventListener("click", selfUpdate); - $("#w-update").addEventListener("click", update); - $("#w-unload").addEventListener("click", unloadAll); - $("#w-os-update").addEventListener("click", osUpdate); - $("#w-reboot").addEventListener("click", rebootServer); - $("#w-console-sel").addEventListener("change", connectConsole); - $("#w-update-cancel").addEventListener("click", cancelActive); - $("#w-swap-check").addEventListener("click", checkSwap); - if (lastStatus) renderSwap(lastStatus); - connectConsole(); -} - -// ---- llama-swap-Transparenz ---- -let lastStatus = null; -function renderSwap(s) { - const chip = $("#w-swap-state"); if (!chip) return; - const ok = !!s?.swap_ok; - chip.textContent = ok ? "● erreichbar" : "● offline"; - chip.style.cssText = "margin-left:auto;" + (ok - ? "background:rgba(63,185,80,.12);border-color:rgba(63,185,80,.25);color:#7ee29a" - : "background:rgba(248,81,73,.12);border-color:rgba(248,81,73,.3);color:#ff9b95"); - $("#w-swap-url").textContent = (s?.swap_url || "—") + "/v1"; - const models = (s?.models || []); - const loaded = models.find(m => m.state === "running" || m.state === "loading"); - $("#w-swap-loaded").innerHTML = loaded - ? `${esc(loaded.name)} (${esc(loaded.state)})` - : `nichts geladen — lädt bei der nächsten Anfrage`; - const real = models.filter(m => !m.incomplete).length; - $("#w-swap-count").textContent = real ? `${real} bereit` : "keine"; -} - -async function checkSwap() { - const res = $("#w-swap-check-res"); res.textContent = "prüfe…"; res.style.color = "var(--mut)"; - try { - const r = await api("/api/integration/test"); - if (r.ok) { res.style.color = "#7ee29a"; res.textContent = `✓ Engine antwortet — ${r.models.length} Modell(e): ${r.models.join(", ")}`; } - else { res.style.color = "#ff9b95"; res.textContent = "✗ keine Verbindung: " + r.error; } - } catch (e) { res.style.color = "#ff9b95"; res.textContent = "✗ " + e.message; } -} - -function onStatus(s) { lastStatus = s; renderSwap(s); } - -async function cancelActive() { - if (!activeJob) return; - if (!await confirmModal({ title: activeJob.label + " abbrechen?", body: "Der laufende Vorgang wird gestoppt. Bei einem Download bleibt eine unvollständige Datei zurück (kannst du später neu starten).", confirmLabel: "Abbrechen", danger: true })) return; - try { await api(`/api/jobs/${activeJob.id}/cancel`, { method: "POST" }); toast("Abbruch angefordert…"); } - catch (e) { toast(e.message, true); } -} - -let ws = null; -function connectConsole() { - if (ws) { ws.close(); ws = null; } - const svc = $("#w-console-sel").value, out = $("#w-console"); - out.textContent = "Verbinde mit " + svc + "…\n"; - const proto = location.protocol === "https:" ? "wss:" : "ws:"; - ws = new WebSocket(`${proto}//${location.host}/api/logs/${svc}?token=${encodeURIComponent(getToken())}`); - ws.onmessage = e => { out.textContent += e.data; out.scrollTop = out.scrollHeight; }; - ws.onclose = () => { out.textContent += "\n— Verbindung getrennt —"; }; -} - -async function restartService(name, title, body) { - if (!await confirmModal({ title, body, confirmLabel: "Neustarten" })) return; - try { await api(`/api/service/${name}/restart`, { method: "POST" }); toast("Neustart ausgelöst: " + name); setTimeout(refreshSoon, 2000); } - catch (e) { toast(e.message, true); } -} - -async function unloadAll() { - if (!await confirmModal({ title: "Grafikspeicher leeren?", body: "Alle geladenen Modelle werden entladen. Sie laden beim nächsten Aufruf automatisch neu — es geht nichts verloren.", confirmLabel: "Leeren" })) return; - try { await api("/api/unload", { method: "POST" }); toast("Grafikspeicher geleert."); setTimeout(refreshSoon, 600); } - catch (e) { toast(e.message, true); } -} - -async function selfUpdate() { - if (!await confirmModal({ title: "Mission Control aktualisieren?", body: "Holt die neueste Dashboard-Version aus Gitea und startet automatisch neu. Die Seite trennt kurz und verbindet von selbst wieder (~10 Sek)." })) return; - try { const r = await api("/api/self-update", { method: "POST" }); toast("Update läuft — Dashboard startet gleich neu."); track(r.job_id); } - catch (e) { toast(e.message, true); } -} - -// ---- Live-Verlauf laufender Vorgaenge (Updates) direkt auf der Server-Seite ---- -let activeJob = null; // { id, label } -const notified = new Set(); // Job-IDs, fuer die schon eine Abschluss-Meldung kam - -function watchJob(id, label) { - activeJob = { id, label }; - const card = $("#w-update-card"); - if (!card) return; - card.style.display = ""; - $("#w-update-label").textContent = label + " läuft…"; - $("#w-update-badge").innerHTML = 'läuft…'; - $("#w-update-log").textContent = "Starte…"; - card.scrollIntoView({ behavior: "smooth", block: "nearest" }); -} - -function onJobs(jobs) { - if (!activeJob) return; - const j = (jobs || []).find(x => x.id === activeJob.id); - const card = $("#w-update-card"); - if (!j || !card) return; - card.style.display = ""; - const logEl = $("#w-update-log"); - if (logEl) { logEl.textContent = (j.log || []).join("\n"); logEl.scrollTop = logEl.scrollHeight; } - const done = j.state === "done", failed = j.state === "failed", canceled = j.state === "canceled"; - const terminal = done || failed || canceled; - const badge = $("#w-update-badge"); - if (badge) badge.innerHTML = done ? '✓ fertig' - : failed ? '✗ fehlgeschlagen' - : canceled ? 'abgebrochen' - : 'läuft…'; - - // Fortschrittsbalken: exakter Backend-Wert (Download) bevorzugt, sonst %-Angabe im Log. - let pval = null; - if (!terminal) { - if (typeof j.progress === "number") { - pval = j.progress; - } else { - const last = (j.log || [])[j.log.length - 1] || ""; - const pm = last.match(/(\d+(?:\.\d+)?)\s*%/); - if (pm) pval = parseFloat(pm[1]); - } - } - const bar = $("#w-update-bar"); - if (bar) { - if (pval != null) { bar.style.display = ""; bar.querySelector("i").style.width = Math.min(100, pval) + "%"; } - else bar.style.display = "none"; - } - $("#w-update-cancel").style.display = terminal ? "none" : ""; - - if (terminal) { - $("#w-update-label").textContent = activeJob.label + - (done ? " — erfolgreich abgeschlossen." : canceled ? " — abgebrochen." : " — fehlgeschlagen (siehe Protokoll)."); - if (!notified.has(j.id)) { - notified.add(j.id); - toast(done ? "✓ " + activeJob.label + " abgeschlossen." - : canceled ? activeJob.label + " abgebrochen." - : "✗ " + activeJob.label + " fehlgeschlagen.", failed); - if (done) setTimeout(refreshSoon, 800); - } - } -} - -async function update() { - if (!await confirmModal({ title: "LLM-Engine aktualisieren?", body: "Lädt die neueste llama.cpp-ROCm-Version und installiert sie. Die Engine ist während des Updates kurz nicht erreichbar." })) return; - try { const r = await api("/api/update", { method: "POST" }); toast("LLM-Engine-Update läuft."); track(r.job_id); watchJob(r.job_id, "LLM-Engine-Update"); } - catch (e) { toast(e.message, true); } -} - -async function osUpdate() { - if (!await confirmModal({ title: "OS-Updates installieren?", body: "Führt apt update & upgrade aus. Das kann ein paar Minuten dauern; der Fortschritt erscheint in der Aktivität." })) return; - const pwd = await promptModal({ title: "sudo-Passwort", body: "Für die System-Updates wird einmalig dein sudo-Passwort gebraucht.", placeholder: "sudo-Passwort", password: true, confirmLabel: "Installieren" }); - if (!pwd) return; - try { const r = await api("/api/os-update", { method: "POST", body: JSON.stringify({ password: pwd }) }); toast("OS-Update gestartet."); track(r.job_id); watchJob(r.job_id, "OS-Update"); } - catch (e) { toast(e.message, true); } -} - -async function rebootServer() { - if (!await confirmModal({ title: "Server wirklich neu starten?", body: "Der ganze Bosgame startet physisch neu. Alles ist für ~1 Minute offline — auch dieses Dashboard.", confirmLabel: "Reboot", danger: true })) return; - const pwd = await promptModal({ title: "sudo-Passwort", body: "Für den Reboot wird einmalig dein sudo-Passwort gebraucht.", placeholder: "sudo-Passwort", password: true, confirmLabel: "Jetzt neustarten", danger: true }); - if (!pwd) return; - try { await api("/api/reboot", { method: "POST", body: JSON.stringify({ password: pwd }) }); toast("Reboot ausgelöst — bis gleich."); } - catch (e) { toast(e.message, true); } -} - -export default { id: "server", mount, onJobs, onStatus }; diff --git a/wird b/wird deleted file mode 100644 index e69de29..0000000