feat(phase-c): Vite + Svelte 5 Infrastruktur + erste 2 Panels migriert
- frontend/ eingerichtet: Vite 6, Svelte 5 (Runes), TypeScript - Stores: status/jobs/system mit $state-Runes (status.svelte.ts etc.) - main.ts ersetzt main.js: bundelt Legacy-Panels (overview/models/server/ jobs/cookbook/connect) + mountet neue Svelte-Panels (guides/news) - GuidesPanel.svelte: statischer Guide mit Akkordeon-Tutorials + Konzept-Grid - NewsPanel.svelte: reaktives Newsboard mit API-Fetch + Upgrade-Vorschlaegen - Vite-Output: static/dist/main.js (committet, kein Build auf dem Server) - index.html: laedt jetzt static/dist/main.js statt static/js/main.js - Build: 118 Module → 134 kB (gzip: 44 kB) Naechste Schritte: Overview → Jobs → Server → Models → Cookbook migrieren. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,3 +2,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.claude/launch.json
|
||||
frontend/node_modules/
|
||||
|
||||
Generated
+1449
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "mission-control-frontend",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"svelte": "^5.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^6.3.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { mount } from 'svelte'
|
||||
import { api, getToken, setToken, getHfToken, setHfToken } from '@core/api.js'
|
||||
import { initNav } from '@core/nav.js'
|
||||
import { statusStore } from './stores/status.svelte.js'
|
||||
import { jobsStore } from './stores/jobs.svelte.js'
|
||||
import GuidesPanel from './panels/GuidesPanel.svelte'
|
||||
import NewsPanel from './panels/NewsPanel.svelte'
|
||||
|
||||
// Legacy panels (bundled via Vite; guides + news sind durch Svelte ersetzt)
|
||||
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'
|
||||
|
||||
const legacyPanels: any[] = [overview, models, server, jobs, cookbook, connect]
|
||||
let prevModelStates: Record<string, string> = {}
|
||||
|
||||
// ---- Swap-Flash ----
|
||||
function flashSwap() {
|
||||
const el = document.getElementById('top-active-text')
|
||||
if (el) { el.classList.add('swap-flash'); setTimeout(() => el.classList.remove('swap-flash'), 900) }
|
||||
}
|
||||
|
||||
// ---- Status verteilen ----
|
||||
function applyStatus(s: any) {
|
||||
const dot = document.getElementById('swdot')
|
||||
const label = document.getElementById('swlabel')
|
||||
const activeText = document.getElementById('top-active-text')
|
||||
|
||||
if (!s) {
|
||||
dot && (dot.className = 'dot off')
|
||||
label && (label.textContent = 'Backend nicht erreichbar')
|
||||
activeText && (activeText.textContent = 'Backend offline')
|
||||
showAlert('Backend nicht erreichbar – läuft uvicorn?', false)
|
||||
} else {
|
||||
const host = (s.swap_url || '').replace(/^https?:\/\//, '')
|
||||
dot && (dot.className = 'dot ' + (s.swap_ok ? 'on' : 'off'))
|
||||
label && (label.textContent = (s.swap_ok ? 'LLM-Engine: Online – ' : 'LLM-Engine: Offline – ') + host)
|
||||
const active = (s.models || []).find((m: any) => m.state === 'running')
|
||||
if (activeText) {
|
||||
activeText.innerHTML = active
|
||||
? `Geladen: <b style="color:var(--teal)">${active.name}</b>`
|
||||
: 'Kein Modell im VRAM'
|
||||
}
|
||||
if (s.swap_ok) hideAlert()
|
||||
else showAlert(`LLM-Engine nicht erreichbar unter <b>${host}</b> – läuft der llama-swap Dienst?`, true)
|
||||
|
||||
// Swap-Erkennung
|
||||
const newStates: Record<string, string> = {}
|
||||
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
|
||||
}
|
||||
setSecChip(s)
|
||||
statusStore.set(s)
|
||||
for (const p of legacyPanels) p.onStatus?.(s)
|
||||
}
|
||||
|
||||
function setSecChip(s: any) {
|
||||
const chip = document.getElementById('sec-chip')
|
||||
const tx = document.getElementById('sec-chip-tx')
|
||||
if (!chip || !tx) return
|
||||
chip.classList.toggle('ok', !!s?.secured)
|
||||
tx.textContent = s?.secured ? 'Geschützt · Heimnetz' : 'Nur im Heimnetz (offen)'
|
||||
}
|
||||
|
||||
function applyJobs(jobs: any[]) {
|
||||
jobsStore.set(jobs || [])
|
||||
for (const p of legacyPanels) p.onJobs?.(jobs)
|
||||
}
|
||||
|
||||
function applySystem(sys: any) {
|
||||
for (const p of legacyPanels) p.onSystem?.(sys)
|
||||
}
|
||||
|
||||
function showAlert(html: string, warn: boolean) {
|
||||
const a = document.getElementById('alert')!
|
||||
a.className = 'alert' + (warn ? ' warn' : '')
|
||||
a.innerHTML = `<span class="a-dot"></span><span>${html}</span>`
|
||||
a.style.display = 'flex'
|
||||
}
|
||||
function hideAlert() {
|
||||
const a = document.getElementById('alert')
|
||||
if (a) a.style.display = 'none'
|
||||
}
|
||||
|
||||
// ---- Update-Badges ----
|
||||
const goView = (v: string) => (document.querySelector(`.nav-item[data-view="${v}"]`) as HTMLElement)?.click()
|
||||
|
||||
function mkBadge(text: string, cls: string, view: string, title?: string) {
|
||||
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 = document.getElementById('update-badges'); if (!el) return
|
||||
const age = u.apt_cache_age_h as number | null
|
||||
const stale = age != null && age > 24
|
||||
const ageNote = age != null ? ` · apt-Cache: vor ${age}h` : ''
|
||||
el.innerHTML = ''
|
||||
el.appendChild(mkBadge(
|
||||
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)` : `Keine Updates${ageNote}`
|
||||
))
|
||||
el.appendChild(mkBadge(u.engine > 0 ? 'Engine: Update' : 'Engine: aktuell', u.engine > 0 ? 'warn' : 'ok', 'server'))
|
||||
el.appendChild(mkBadge(u.models > 0 ? `Modelle: ${u.models}` : 'Modelle: aktuell', u.models > 0 ? 'warn' : 'ok', 'news'))
|
||||
} catch { /* noop */ }
|
||||
}
|
||||
|
||||
// ---- Polling ----
|
||||
async function pollStatus() {
|
||||
try { applyStatus(await api('/api/status')) }
|
||||
catch { applyStatus(null) }
|
||||
}
|
||||
async function pollJobs() {
|
||||
try { applyJobs(await api('/api/jobs')) }
|
||||
catch { /* noop */ }
|
||||
}
|
||||
|
||||
function connectSystemStream() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const ws = new WebSocket(`${protocol}//${window.location.host}/api/system/stream`)
|
||||
ws.onmessage = e => { try { applySystem(JSON.parse(e.data)) } catch {} }
|
||||
ws.onclose = () => { setTimeout(connectSystemStream, 3000) }
|
||||
ws.onerror = () => ws.close()
|
||||
}
|
||||
|
||||
// ---- Boot ----
|
||||
|
||||
// Legacy panels mounten (overview, models, server, jobs, cookbook, connect)
|
||||
for (const p of legacyPanels) p.mount?.()
|
||||
|
||||
// Svelte panels mounten (guides, news)
|
||||
const guidesEl = document.querySelector('.view[data-view="guides"]')
|
||||
if (guidesEl) mount(GuidesPanel, { target: guidesEl })
|
||||
|
||||
const newsEl = document.querySelector('.view[data-view="news"]')
|
||||
if (newsEl) mount(NewsPanel, { target: newsEl })
|
||||
|
||||
initNav('overview')
|
||||
|
||||
// Token / Settings
|
||||
const tokenInput = document.getElementById('token') as HTMLInputElement | null
|
||||
if (tokenInput) {
|
||||
tokenInput.value = getToken()
|
||||
tokenInput.addEventListener('change', e => { setToken((e.target as HTMLInputElement).value); pollStatus() })
|
||||
}
|
||||
const hfInput = document.getElementById('hf-token') as HTMLInputElement | null
|
||||
if (hfInput) {
|
||||
hfInput.value = getHfToken()
|
||||
hfInput.addEventListener('change', e => setHfToken((e.target as HTMLInputElement).value))
|
||||
}
|
||||
const sbtn = document.getElementById('nav-settings')
|
||||
const smod = document.getElementById('settings-modal')
|
||||
const scls = document.getElementById('sm-close')
|
||||
if (sbtn && smod && scls) {
|
||||
sbtn.addEventListener('click', () => (smod!.style.display = 'flex'))
|
||||
scls.addEventListener('click', () => (smod!.style.display = 'none'))
|
||||
}
|
||||
|
||||
function tickClock() {
|
||||
const c = document.getElementById('clock')
|
||||
if (c) c.textContent = new Date().toTimeString().slice(0, 5)
|
||||
}
|
||||
|
||||
document.addEventListener('mc:refresh', pollStatus as EventListener)
|
||||
pollStatus(); pollJobs(); connectSystemStream()
|
||||
setInterval(tickClock, 1000)
|
||||
setInterval(pollStatus, 3000)
|
||||
setInterval(pollJobs, 1500)
|
||||
pollUpdates()
|
||||
setInterval(pollUpdates, 60000)
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { TUTORIALS, CONCEPTS } from './guides-data.js'
|
||||
</script>
|
||||
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h1>Guide für Einsteiger</h1>
|
||||
<div class="sub">Praktische Workflows + die Local-LLM-Welt in Klartext — ohne Vorwissen.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-h"><h3>Tutorials & Workflows</h3></div>
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{#each TUTORIALS as [title, body], i}
|
||||
<details class="guide-acc" open={i === 0}>
|
||||
<summary>{title}</summary>
|
||||
<div class="acc-body">{@html body}</div>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="card-h" style="margin-top:8px"><h3>Konzepte kurz erklärt</h3></div>
|
||||
<div class="concept-grid">
|
||||
{#each CONCEPTS as [term, text]}
|
||||
<div class="concept">
|
||||
<h4>{term}</h4>
|
||||
<p>{text}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { api, getHfToken } from '@core/api.js'
|
||||
|
||||
let items = $state<any[]>([])
|
||||
let upgrades = $state<any[]>([])
|
||||
let loadingNews = $state(true)
|
||||
let newsError = $state('')
|
||||
|
||||
const fitCls = (l: string) => l === 'perfect' ? 'ok' : l === 'marginal' ? 'warn' : 'bad'
|
||||
|
||||
function ago(ts: number) {
|
||||
const s = Math.max(0, Date.now() / 1000 - ts)
|
||||
if (s < 60) return s + 's'
|
||||
if (s < 3600) return Math.floor(s / 60) + 'm'
|
||||
if (s < 86400) return Math.floor(s / 3600) + 'h'
|
||||
return Math.floor(s / 86400) + 'd'
|
||||
}
|
||||
|
||||
async function loadNews() {
|
||||
loadingNews = true; newsError = ''
|
||||
try {
|
||||
items = (await api('/api/news')).items || []
|
||||
} catch (e: any) {
|
||||
newsError = e.message || 'Fehler beim Laden'
|
||||
} finally {
|
||||
loadingNews = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUpgrades() {
|
||||
try {
|
||||
upgrades = (await api('/api/cookbook/upgrades')).upgrades || []
|
||||
} catch {
|
||||
upgrades = []
|
||||
}
|
||||
}
|
||||
|
||||
async function installUpgrade(u: any, btn: HTMLButtonElement) {
|
||||
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() })
|
||||
})
|
||||
document.querySelector(".nav-item[data-view='activity']")?.dispatchEvent(new MouseEvent('click'))
|
||||
} catch (e: any) {
|
||||
btn.disabled = false; btn.textContent = 'Installieren'
|
||||
alert('Fehler: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
function refresh() { loadNews(); loadUpgrades() }
|
||||
|
||||
onMount(() => { loadNews(); loadUpgrades() })
|
||||
</script>
|
||||
|
||||
<div class="pagehead">
|
||||
<div>
|
||||
<h1>News</h1>
|
||||
<div class="sub">Aktuelles aus der Local-LLM-Welt — aus kuratierten Quellen.</div>
|
||||
</div>
|
||||
<button class="ghost" onclick={refresh}>Aktualisieren</button>
|
||||
</div>
|
||||
|
||||
{#if upgrades.length}
|
||||
<div class="card" style="margin-bottom:18px;border-color:rgba(45,212,191,.25)">
|
||||
<div class="card-h">
|
||||
<h3>Für dich: Upgrades</h3>
|
||||
<span class="meta">{upgrades.length}</span>
|
||||
</div>
|
||||
<div class="card-sub">Neuere Modelle, die ein installiertes ersetzen können.</div>
|
||||
<div class="list">
|
||||
{#each upgrades as u}
|
||||
<div class="li" style="align-items:flex-start">
|
||||
<div class="li-main">
|
||||
<div class="flex items-center gap-2" style="flex-wrap:wrap">
|
||||
<span class="li-id" style="font-family:var(--sans);font-weight:500">{u.name}</span>
|
||||
<span class="fit-badge {fitCls(u.fit.level)}">{u.fit.text}</span>
|
||||
</div>
|
||||
<div class="li-sub">Ersetzt dein „{u.old}". {u.why}</div>
|
||||
</div>
|
||||
<button class="primary" onclick={(e) => installUpgrade(u, e.currentTarget as HTMLButtonElement)}>
|
||||
Installieren
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loadingNews}
|
||||
<div class="empty" style="text-align:center;padding:40px">Lade News… (erstes Laden kann kurz dauern)</div>
|
||||
{:else if newsError}
|
||||
<div class="alert err" style="margin:0">{newsError}</div>
|
||||
{:else if !items.length}
|
||||
<div class="empty-c">
|
||||
<div class="e-t">Keine News geladen</div>
|
||||
<div class="e-s">Die Quellen sind gerade nicht erreichbar — später nochmal probieren.</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="news-grid">
|
||||
{#each items as n, i}
|
||||
<a href={n.link} target="_blank" rel="noopener" class="news-card{i === 0 ? ' featured' : ''}">
|
||||
<div class="news-meta">
|
||||
<span class="news-src">{n.source}</span>
|
||||
{#if n.relevant}<span class="news-rel">für dich</span>{/if}
|
||||
<span class="news-date">{n.ts ? ago(n.ts) : ''}</span>
|
||||
</div>
|
||||
<div class="news-title">{n.title}</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,86 @@
|
||||
export const TUTORIALS: [string, string][] = [
|
||||
["Dein erster Workflow — in 3 Schritten", `
|
||||
<ol class="tut-steps">
|
||||
<li><b>Modell holen:</b> Tab <b>Cookbook</b> → wähle deinen Anwendungsfall (z.B. „Coden") →
|
||||
<b>Komplettes Setup installieren</b>. Der Download läuft im Tab <b>Aktivität</b>.</li>
|
||||
<li><b>Verbinden:</b> Tab <b>Verbinden</b> → Basis-URL kopieren → in deinem Tool (z.B. OpenCode)
|
||||
als „OpenAI-kompatibel" einfügen. <b>Verbindung testen</b> bestätigt, dass es läuft.</li>
|
||||
<li><b>Arbeiten:</b> Im Tool das Modell (z.B. <code>coder</code>) wählen und loslegen. Das passende
|
||||
Modell wird <b>automatisch geladen</b> — du musst nichts starten.</li>
|
||||
</ol>`],
|
||||
["Wie leere ich den Kontext? Machen die Tools das selbst?", `
|
||||
<p><b>Kurz: meistens ja, automatisch.</b> Der Kontext (das „Gedächtnis") gilt pro Unterhaltung.
|
||||
Startest du im Tool einen <b>neuen Chat / eine neue Session</b>, ist der Kontext wieder leer —
|
||||
das Tool verwaltet das Fenster für dich.</p>
|
||||
<p>Du musst nur selten eingreifen: wird ein Gespräch sehr lang, fang einen neuen an. <b>Wichtig:</b>
|
||||
„Grafikspeicher leeren" (Tab Server) ist etwas anderes — das wirft das <i>Modell</i> aus dem Speicher,
|
||||
nicht den Gesprächsverlauf.</p>`],
|
||||
["Was passiert, wenn der Kontext volläuft?", `
|
||||
<p>Ist das Kontextfenster voll, passiert je nach Tool eins von dreien: das Modell <b>vergisst den
|
||||
Anfang</b> (älteste Nachrichten fallen raus), das Tool <b>kürzt automatisch</b>, oder du bekommst
|
||||
einen Fehler wie „context length exceeded".</p>
|
||||
<p><b>So gehst du vor:</b></p>
|
||||
<ol class="tut-steps">
|
||||
<li><b>Neuen Chat starten</b> — der einfachste Reset, Kontext ist wieder leer.</li>
|
||||
<li><b>Zusammenfassen lassen:</b> „Fasse unseren Stand kurz zusammen", dann im neuen Chat einfügen.</li>
|
||||
<li><b>Kontext vergrößern:</b> Tab <b>Modelle</b> → <b>Konfigurieren</b> → wir zeigen das Optimum
|
||||
für deine Hardware (mehr Kontext = mehr Speicherbedarf).</li>
|
||||
</ol>`],
|
||||
["Was ist ein Agent — und wann brauche ich einen?", `
|
||||
<p>Ein nacktes Modell <b>schreibt nur Text zurück</b>. Ein <b>Agent</b> ist ein Programm drumherum,
|
||||
das das Modell <b>handeln</b> lässt: Dateien lesen und ändern, Befehle ausführen, im Web suchen —
|
||||
Schritt für Schritt in einer Schleife, bis die Aufgabe erledigt ist.</p>
|
||||
<p><b>Wann lohnt sich was?</b></p>
|
||||
<ul class="tut-steps" style="list-style:disc">
|
||||
<li><b>Fragen, Texte, Erklärungen, Brainstorming</b> → kein Agent nötig, eine Chat-Oberfläche
|
||||
wie <b>OpenWebUI</b> reicht völlig.</li>
|
||||
<li><b>Coden über mehrere Dateien, Befehle ausführen, recherchieren + umsetzen</b>
|
||||
→ <b>Agent</b> (OpenCode, Cline, Continue).</li>
|
||||
</ul>
|
||||
<p><b>Gut zu wissen:</b> Diese Agenten <b>laufen lokal auf deinem PC</b> — du brauchst
|
||||
<b>keinen</b> Cloud-Dienst.</p>`],
|
||||
["Was sind Skills — und wie installiere ich sie?", `
|
||||
<p>Ein <b>Skill</b> ist <b>verpacktes Können</b>: eine kurze Anleitung, die dem Agenten beibringt,
|
||||
eine wiederkehrende Aufgabe jedes Mal so zu erledigen, wie <i>du</i> es willst.</p>
|
||||
<p><b>Skill vs. MCP:</b> <b>MCP</b> gibt dem Agenten neue <b>Werkzeuge</b>. <b>Skills</b> geben
|
||||
ihm <b>Know-how</b>. Kurz: <b>Werkzeuge = MCP, Anleitungen = Skills.</b></p>
|
||||
<p><b>Installieren:</b> meist nur eine Datei ablegen — kein Build, kein Neustart:</p>
|
||||
<ol class="tut-steps">
|
||||
<li><b>Pro Projekt:</b> in den Projektordner (z.B. <code>AGENTS.md</code>, <code>.clinerules/</code>).</li>
|
||||
<li><b>Global:</b> in den Konfig-Ordner des Tools — gilt überall.</li>
|
||||
</ol>`],
|
||||
["MCP — deinem Agenten echte Werkzeuge geben", `
|
||||
<p><b>MCP</b> (Model Context Protocol) ist ein einheitlicher „Stecker", über den ein Agent zusätzliche
|
||||
<b>Werkzeuge</b> bekommt: das Web durchsuchen, Dateien lesen, eine Datenbank abfragen.</p>
|
||||
<p>Du fügst in deinem Agenten einen „MCP-Server" hinzu (eine Zeile in der Konfiguration je Werkzeug).
|
||||
Danach entscheidet der Agent selbst, wann er das Werkzeug einsetzt.</p>
|
||||
<ul class="tut-steps" style="list-style:disc">
|
||||
<li><b>Faustregel:</b> Fehlt dem Agenten eine <b>Fähigkeit</b> → MCP. Soll er etwas nur
|
||||
<b>besser machen</b> → Skill.</li>
|
||||
</ul>`],
|
||||
["Auto-Swap — wie das Laden funktioniert", `
|
||||
<p>llama-swap lädt automatisch das Modell, das gerade angefragt wird, und entlädt es nach einer
|
||||
Leerlaufzeit wieder. <b>Es läuft immer nur eines gleichzeitig.</b></p>
|
||||
<p>Du richtest beliebig viele Modelle ein und wählst im Tool das passende —
|
||||
das Laden/Entladen passiert im Hintergrund, ohne dass du etwas tun musst.</p>`],
|
||||
["Die empfohlenen Tools optimal nutzen", `
|
||||
<ul class="tut-steps" style="list-style:disc">
|
||||
<li><b>OpenCode:</b> Autonomes Coden im Terminal/Editor — ideal für „bau mir Feature X".</li>
|
||||
<li><b>Cline:</b> Dasselbe als VS-Code-Erweiterung, mit Bestätigung vor jeder Aktion.</li>
|
||||
<li><b>OpenWebUI:</b> Klassische Chat-Oberfläche für Fragen/Texte.</li>
|
||||
</ul>
|
||||
<p><b>Tipp:</b> starkes Modell (<code>coder</code>) für schwere Aufgaben, schnelles
|
||||
(<code>scout</code>) für einfache — du wählst es einfach im Tool aus.</p>`],
|
||||
]
|
||||
|
||||
export const CONCEPTS: [string, string][] = [
|
||||
["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)."],
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
let _value = $state<any[]>([])
|
||||
|
||||
export const jobsStore = {
|
||||
get value() { return _value },
|
||||
set(jobs: any[]) { _value = jobs || [] },
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
let _value = $state<any>(null)
|
||||
|
||||
export const statusStore = {
|
||||
get value() { return _value },
|
||||
set(s: any) { _value = s },
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
let _value = $state<any>(null)
|
||||
|
||||
export const systemStore = {
|
||||
get value() { return _value },
|
||||
set(sys: any) { _value = sys },
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
preprocess: vitePreprocess(),
|
||||
compilerOptions: {
|
||||
runes: true,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": false,
|
||||
"noEmit": true,
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"allowJs": true,
|
||||
"checkJs": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
base: '/static/dist/',
|
||||
build: {
|
||||
outDir: resolve(__dirname, '../static/dist'),
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: resolve(__dirname, 'src/main.ts'),
|
||||
output: {
|
||||
entryFileNames: 'main.js',
|
||||
chunkFileNames: '[name].js',
|
||||
assetFileNames: '[name][extname]',
|
||||
// Alles in eine Datei — kein dynamisches Chunking für einfaches Deploy
|
||||
manualChunks: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@core': resolve(__dirname, '../static/js/core'),
|
||||
'@panels': resolve(__dirname, '../static/js/panels'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Vendored
+557
File diff suppressed because one or more lines are too long
+1
-1
@@ -121,6 +121,6 @@
|
||||
document.getElementById("logo").innerHTML = ICON.logo;
|
||||
document.querySelectorAll("[data-ic]").forEach(n => (n.innerHTML = ICON[n.dataset.ic] || ""));
|
||||
</script>
|
||||
<script type="module" src="/static/js/main.js"></script>
|
||||
<script type="module" src="/static/dist/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user