All checks were successful
release-tag / release-image (push) Successful in 3m42s
242 lines
67 KiB
HTML
242 lines
67 KiB
HTML
<!doctype html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||
<title>NeuroForge · Knowledge OS</title>
|
||
<style>
|
||
:root{
|
||
color-scheme:dark;--bg:#080b12;--panel:#101622;--panel2:#151d2c;--line:#26334a;--muted:#91a0b7;--text:#eef3fb;
|
||
--accent:#7aa7ff;--accent2:#65d6b5;--warn:#f7c36d;--bad:#ff7c8d;--shadow:0 18px 48px rgba(0,0,0,.28);
|
||
font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif
|
||
}
|
||
*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 15% 0,#10192b 0,#080b12 36rem);color:var(--text)}
|
||
button,input,textarea,select{font:inherit}button{cursor:pointer}.app{min-height:100vh;display:grid;grid-template-columns:238px minmax(0,1fr)}
|
||
.sidebar{position:sticky;top:0;height:100vh;padding:22px 16px;border-right:1px solid var(--line);background:rgba(8,11,18,.9);backdrop-filter:blur(16px);z-index:20}
|
||
.brand{display:flex;align-items:center;gap:12px;padding:4px 8px 24px}.mark{width:36px;height:36px;border-radius:12px;background:linear-gradient(145deg,var(--accent),#8d7cff);box-shadow:0 8px 24px #637cff45}.brand b{font-size:20px}.brand small{display:block;color:var(--muted);margin-top:2px}
|
||
.nav{display:grid;gap:5px}.nav button{display:flex;align-items:center;gap:10px;width:100%;border:0;background:transparent;color:var(--muted);padding:10px 12px;border-radius:11px;text-align:left}.nav button:hover{background:#151e2e;color:var(--text)}.nav button.active{background:#1b2840;color:#fff;box-shadow:inset 3px 0 var(--accent)}.nav .ico{width:20px;text-align:center}
|
||
.sidebarFoot{position:absolute;left:16px;right:16px;bottom:18px}.statusChip{display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:#0e1420;color:var(--muted);font-size:13px}.dot{width:8px;height:8px;border-radius:50%;background:var(--muted)}.dot.ok{background:var(--accent2);box-shadow:0 0 12px #65d6b566}.dot.bad{background:var(--bad)}
|
||
.main{min-width:0;padding:26px 30px 60px}.topbar{display:flex;justify-content:space-between;gap:16px;align-items:center;margin-bottom:22px}.topbar h1{font-size:25px;margin:0}.topbar p{margin:5px 0 0;color:var(--muted);font-size:14px}.actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||
.btn{border:1px solid transparent;border-radius:10px;padding:9px 13px;font-weight:650;background:var(--accent);color:#07101e}.btn:hover{filter:brightness(1.06)}.btn.secondary{background:#172133;border-color:#2d3c55;color:var(--text)}.btn.ghost{background:transparent;border-color:var(--line);color:var(--muted)}.btn.danger{background:#3a1821;border-color:#65303e;color:#ffb8c2}.btn:disabled{opacity:.45;cursor:not-allowed}
|
||
.view{display:none}.view.active{display:block}.grid{display:grid;gap:14px}.metrics{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:14px}.metric{border:1px solid var(--line);background:linear-gradient(160deg,#131b2a,#0e141f);border-radius:16px;padding:17px;box-shadow:var(--shadow)}.metric .label{color:var(--muted);font-size:12px;text-transform:uppercase;letter-spacing:.06em}.metric .value{font-size:28px;font-weight:750;margin-top:5px}.metric .sub{font-size:12px;color:var(--muted);margin-top:4px}
|
||
.card{border:1px solid var(--line);background:rgba(16,22,34,.94);border-radius:16px;padding:18px;box-shadow:var(--shadow)}.card h2{font-size:17px;margin:0 0 14px}.card h3{font-size:14px;margin:18px 0 9px}.two{grid-template-columns:minmax(0,1.35fr) minmax(300px,.65fr)}.three{grid-template-columns:repeat(3,minmax(0,1fr))}.split{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(320px,.72fr);gap:14px}
|
||
label{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.field{margin-bottom:12px}.control{width:100%;border:1px solid #30405b;background:#0b111c;color:var(--text);border-radius:10px;padding:10px 11px;outline:none}.control:focus{border-color:var(--accent);box-shadow:0 0 0 3px #7aa7ff18}textarea.control{min-height:120px;resize:vertical}.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}.row{display:flex;gap:10px;align-items:center}.row>*{min-width:0}.grow{flex:1}.checks{display:flex;gap:14px;flex-wrap:wrap}.checks label{display:flex;align-items:center;gap:7px;color:var(--text);font-size:13px;margin:0}.checks input{accent-color:var(--accent)}
|
||
.pill{display:inline-flex;align-items:center;gap:5px;padding:4px 8px;border-radius:99px;background:#1a2639;color:#b9c8dd;font-size:11px;border:1px solid #2c3b55}.pill.good{background:#102b25;border-color:#225445;color:#8de0c5}.pill.warn{background:#2b2414;border-color:#5b4923;color:#f5cf85}.pill.bad{background:#321821;border-color:#66303e;color:#ffacb8}.muted{color:var(--muted)}.small{font-size:12px}.error{color:#ffadb8}.good{color:#83dec1}.warn{color:#f7ce84}.pre{white-space:pre-wrap;word-break:break-word;background:#090e17;border:1px solid #202c40;border-radius:10px;padding:11px;max-height:360px;overflow:auto}.scroll{max-height:590px;overflow:auto}.list{display:grid;gap:8px}.item{border:1px solid #26344a;background:#0d1420;border-radius:12px;padding:12px}.item.click{cursor:pointer}.item.click:hover{border-color:#3c5377;background:#111b2b}.itemTitle{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}.itemTitle b{font-size:13px}.item p{margin:6px 0;color:#ccd7e7;font-size:13px;line-height:1.42}.meta{display:flex;gap:6px;flex-wrap:wrap;color:var(--muted);font-size:11px}
|
||
.progress{height:7px;border-radius:99px;background:#1a2536;overflow:hidden}.progress span{display:block;height:100%;background:linear-gradient(90deg,var(--accent),var(--accent2));border-radius:inherit}
|
||
.pipeline{display:grid;grid-template-columns:repeat(7,minmax(120px,1fr));gap:8px}.stage{position:relative;padding:12px;border:1px solid var(--line);border-radius:12px;background:#0d1420}.stage b{display:block;font-size:12px}.stage small{display:block;color:var(--muted);margin-top:4px}.stage:not(:last-child):after{content:"→";position:absolute;right:-8px;top:28px;color:var(--accent);z-index:2}
|
||
.graphShell{position:relative;border:1px solid var(--line);border-radius:16px;background:radial-gradient(circle at center,#121d30,#090d15 72%);overflow:hidden;min-height:620px}.graphShell canvas{width:100%;height:620px;display:block;touch-action:none}.graphHud{position:absolute;left:12px;top:12px;display:flex;gap:7px;flex-wrap:wrap;pointer-events:none}.graphHud .pill{background:#0b111ddd;backdrop-filter:blur(8px)}.graphTools{position:absolute;right:12px;top:12px;display:flex;gap:6px}.graphTools button{width:34px;height:34px;border-radius:10px;padding:0}.graphHint{position:absolute;left:12px;bottom:10px;color:#8697b1;font-size:11px;background:#080d16c7;padding:5px 8px;border-radius:8px}.legend{display:flex;gap:10px;flex-wrap:wrap;margin:9px 0 0}.legend span{font-size:11px;color:var(--muted)}.sw{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}
|
||
.drop{border:1px dashed #405575;border-radius:14px;padding:25px;text-align:center;background:#0b121e;transition:.15s}.drop.drag{border-color:var(--accent);background:#111d31}.drop input{max-width:100%}.sourceGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.sourceIcon{width:34px;height:34px;display:grid;place-items:center;border-radius:10px;background:#1a2639;color:var(--accent)}
|
||
.goalGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.goal{border:1px solid var(--line);border-radius:14px;padding:15px;background:#0d1420}.goalHead{display:flex;justify-content:space-between;gap:12px}.goal h3{margin:0 0 4px;font-size:15px}.goal .progress{margin:12px 0 7px}.goalActions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}
|
||
.researchLive{display:none;margin-top:14px;overflow:hidden}.researchLive.show{display:block}.liveHead{display:flex;justify-content:space-between;gap:14px;align-items:flex-start}.liveTitle{display:flex;gap:10px;align-items:center}.livePulse{width:10px;height:10px;border-radius:50%;background:var(--muted);flex:0 0 auto;margin-top:5px}.livePulse.running{background:var(--accent2);box-shadow:0 0 0 0 rgba(101,214,181,.5);animation:researchPulse 1.6s infinite}.livePulse.warn{background:var(--warn)}.livePulse.error{background:var(--bad)}@keyframes researchPulse{0%{box-shadow:0 0 0 0 rgba(101,214,181,.45)}70%{box-shadow:0 0 0 9px rgba(101,214,181,0)}100%{box-shadow:0 0 0 0 rgba(101,214,181,0)}}
|
||
.liveStats{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:8px;margin:14px 0}.liveStat{border:1px solid #24334a;border-radius:11px;background:#0b121e;padding:10px}.liveStat b{font-size:19px;display:block}.liveStat span{font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)}.queryChips{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0 14px}.queryChip{font-size:11px;padding:6px 8px;border:1px solid #30435f;border-radius:9px;background:#0d1726;color:#c8d7ec;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
.eventBoard{display:grid;grid-template-columns:repeat(auto-fit,minmax(245px,1fr));gap:9px}.eventLane{border:1px solid #223149;border-radius:13px;background:#0a101a;min-height:180px;overflow:hidden}.eventLaneHead{display:flex;align-items:center;justify-content:space-between;padding:10px 11px;border-bottom:1px solid #223149;background:#0f1724;position:sticky;top:0;z-index:1}.eventLaneHead b{font-size:12px}.eventLaneBody{padding:7px;display:grid;gap:6px;max-height:360px;overflow:auto}.researchEvent{border:1px solid #25354c;border-radius:9px;background:#0d1521;padding:8px 9px;animation:eventIn .18s ease-out}.researchEvent.running{border-color:#2e6959}.researchEvent.error{border-color:#65303e}.researchEvent.warn,.researchEvent.skipped{border-color:#624e26}.researchEvent .eventTop{display:flex;justify-content:space-between;gap:8px;font-size:10px;color:var(--muted)}.researchEvent b{font-size:11px;display:block;margin:4px 0}.researchEvent p{font-size:11px;line-height:1.35;margin:3px 0;color:#c9d5e6}.researchEvent a{color:var(--accent);font-size:10px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.researchEvent .preview{border-left:2px solid #304866;padding-left:6px;color:#aebdd1}.emptyLane{color:#687990;font-size:11px;padding:10px}@keyframes eventIn{from{transform:translateY(3px);opacity:.45}to{transform:none;opacity:1}}
|
||
.researchHistory{margin-top:12px;display:flex;gap:7px;overflow:auto;padding-bottom:2px}.historyRun{min-width:205px;border:1px solid #26354d;border-radius:10px;background:#0c1420;padding:9px}.historyRun b{font-size:11px}.historyRun .meta{margin-top:5px}
|
||
.tableWrap{overflow:auto}table{width:100%;border-collapse:collapse;font-size:12px}th,td{text-align:left;padding:9px;border-bottom:1px solid #223047;vertical-align:top}th{color:var(--muted);font-weight:600}.kv{display:grid;grid-template-columns:130px 1fr;gap:7px 12px;font-size:12px}.kv div:nth-child(odd){color:var(--muted)}
|
||
.toast{position:fixed;right:20px;bottom:20px;max-width:440px;padding:12px 14px;border:1px solid #334766;border-radius:12px;background:#111b2b;color:#eef3fb;box-shadow:var(--shadow);z-index:80;display:none}.toast.show{display:block}.toast.bad{border-color:#673241;background:#28131a}
|
||
.mobileNav{display:none}
|
||
@media(max-width:1100px){.metrics{grid-template-columns:repeat(2,1fr)}.liveStats{grid-template-columns:repeat(3,1fr)}.pipeline{grid-template-columns:repeat(4,1fr)}.pipeline .stage:nth-child(4):after{display:none}.split,.two{grid-template-columns:1fr}.graphShell canvas{height:560px}.graphShell{min-height:560px}}
|
||
@media(max-width:760px){.app{display:block}.liveStats{grid-template-columns:repeat(2,1fr)}.eventLaneBody{max-height:280px}.sidebar{display:none}.main{padding:18px 14px 90px}.topbar{align-items:flex-start}.topbar h1{font-size:21px}.topbar .actions .btn.ghost{display:none}.metrics{grid-template-columns:1fr 1fr}.metric .value{font-size:23px}.three,.goalGrid,.sourceGrid{grid-template-columns:1fr}.pipeline{grid-template-columns:1fr 1fr}.pipeline .stage:nth-child(even):after{display:none}.pipeline .stage:nth-child(4):after{display:none}.graphShell,.graphShell canvas{min-height:500px;height:500px}.mobileNav{position:fixed;display:flex;left:8px;right:8px;bottom:8px;z-index:60;background:#0c121ddd;border:1px solid #29384f;border-radius:15px;padding:6px;backdrop-filter:blur(18px);overflow-x:auto}.mobileNav button{flex:1;min-width:64px;border:0;background:transparent;color:var(--muted);padding:8px 6px;border-radius:9px;font-size:10px}.mobileNav button.active{background:#1b2840;color:#fff}.kv{grid-template-columns:100px 1fr}}
|
||
@media(max-width:430px){.metrics{grid-template-columns:1fr}.pipeline{grid-template-columns:1fr}.stage:after{display:none!important}.row.stack{flex-direction:column;align-items:stretch}}
|
||
.goalPaused{opacity:.78;border-style:dashed}.pill.warn{color:#ffd27d;border-color:rgba(255,210,125,.35);background:rgba(255,210,125,.08)}</style>
|
||
</head>
|
||
<body>
|
||
<div class="app">
|
||
<aside class="sidebar">
|
||
<div class="brand"><div class="mark"></div><div><b>NeuroForge</b><small>Knowledge OS · v0.8</small></div></div>
|
||
<nav class="nav" id="nav">
|
||
<button class="active" data-view="dashboard"><span class="ico">◈</span>Übersicht</button>
|
||
<button data-view="knowledge"><span class="ico">◎</span>Wissensraum</button>
|
||
<button data-view="sources"><span class="ico">↥</span>Quellen & Import</button>
|
||
<button data-view="research"><span class="ico">⌕</span>Research</button>
|
||
<button data-view="goals"><span class="ico">◉</span>Ziele & Autonomie</button>
|
||
<button data-view="chat"><span class="ico">◇</span>Chat</button>
|
||
<button data-view="models"><span class="ico">⬡</span>Modelle</button>
|
||
<button data-view="ops"><span class="ico">⚙</span>Betrieb</button>
|
||
</nav>
|
||
<div class="sidebarFoot"><div class="statusChip"><span class="dot" id="sideDot"></span><span id="sideStatus">nicht verbunden</span></div></div>
|
||
</aside>
|
||
<main class="main">
|
||
<header class="topbar"><div><h1 id="pageTitle">Übersicht</h1><p id="pageSub">Was das System weiß, lernt und gerade tut.</p></div><div class="actions"><button class="btn ghost" id="refreshBtn">Aktualisieren</button><button class="btn secondary" id="tokenBtn">Admin-Token</button></div></header>
|
||
|
||
<section class="view active" id="view-dashboard">
|
||
<div class="grid metrics">
|
||
<div class="metric"><div class="label">Memories</div><div class="value" id="mMem">–</div><div class="sub" id="mTypes">Wissensknoten</div></div>
|
||
<div class="metric"><div class="label">Synapsen</div><div class="value" id="mSyn">–</div><div class="sub">gewichtete Beziehungen</div></div>
|
||
<div class="metric"><div class="label">Quellen</div><div class="value" id="mSources">–</div><div class="sub">Dokumente · Text · Web</div></div>
|
||
<div class="metric"><div class="label">Aktive Ziele</div><div class="value" id="mGoals">–</div><div class="sub" id="mNextGoal">autonomer Lernplan</div></div>
|
||
</div>
|
||
<div class="grid two">
|
||
<div class="card"><h2>Lernpfad</h2><div class="pipeline">
|
||
<div class="stage"><b>1 · Quelle</b><small>Chat, Text, Dokument, Web</small></div>
|
||
<div class="stage"><b>2 · Evidence</b><small>Extrahieren, chunken, deduplizieren</small></div>
|
||
<div class="stage"><b>3 · Embedding</b><small>stabiler semantischer Raum</small></div>
|
||
<div class="stage"><b>4 · Recall</b><small>HNSW + PQ + Synapsen</small></div>
|
||
<div class="stage"><b>5 · Reason</b><small>Actor/Critic mit Evidenz</small></div>
|
||
<div class="stage"><b>6 · Learn</b><small>Reward + Provenance + Konflikte</small></div>
|
||
<div class="stage"><b>7 · Consolidate</b><small>Episoden → Langzeitwissen</small></div>
|
||
</div><p class="muted small">Web-Ergebnisse und Dokumente werden als quellengebundene Evidence-Memories gespeichert. Erst der normale Recall-/Konsolidierungspfad macht daraus stärkeres semantisches Wissen.</p></div>
|
||
<div class="card"><h2>Systemzustand</h2><div id="systemState" class="list"></div></div>
|
||
</div>
|
||
<div class="grid two" style="margin-top:14px"><div class="card"><h2>Letzte Lernereignisse</h2><div class="list scroll" id="dashEvents"></div></div><div class="card"><h2>Aktive Ziele</h2><div class="list" id="dashGoals"></div></div></div>
|
||
</section>
|
||
|
||
<section class="view" id="view-knowledge">
|
||
<div class="row stack" style="margin-bottom:12px"><div class="grow"><input id="knowledgeQuery" class="control" placeholder="Wissen semantisch durchsuchen …"></div><button class="btn" id="knowledgeSearchBtn">Suchen</button><button class="btn secondary" id="graphReloadBtn">Graph neu laden</button></div>
|
||
<div class="split">
|
||
<div><div class="graphShell"><canvas id="knowledgeCanvas"></canvas><div class="graphHud"><span class="pill" id="lodBadge">LOD · Übersicht</span><span class="pill" id="graphCount">0 Knoten</span><span class="pill" id="graphZoom">100%</span></div><div class="graphTools"><button class="btn secondary" id="zoomInBtn">+</button><button class="btn secondary" id="zoomOutBtn">−</button><button class="btn secondary" id="fitBtn">⌂</button></div><div class="graphHint">Mausrad/Pinch: Zoom · Ziehen: Pan · Klick: Memory öffnen · LOD blendet Details progressiv ein</div></div><div class="legend"><span><i class="sw" style="background:#7aa7ff"></i>semantic</span><span><i class="sw" style="background:#65d6b5"></i>episodic</span><span><i class="sw" style="background:#f7c36d"></i>procedural</span><span><i class="sw" style="background:#bd8cff"></i>working/evidence</span></div></div>
|
||
<div class="card"><h2>Inspektor</h2><div id="knowledgeInspector" class="scroll"><p class="muted">Klicke einen Knoten oder suche nach einem Begriff. Hier siehst du Herkunft, Confidence, Reward, Verknüpfungen und Lernhistorie.</p></div></div>
|
||
</div>
|
||
<div class="card" style="margin-top:14px"><h2>Recall-Erklärung</h2><div id="knowledgeHits" class="list"></div></div>
|
||
</section>
|
||
|
||
<section class="view" id="view-sources">
|
||
<div class="grid two">
|
||
<div class="card"><h2>Dokument importieren</h2><div class="drop" id="dropZone"><p><b>Datei hier ablegen</b> oder auswählen</p><p class="muted small">TXT · Markdown · HTML · JSON · CSV/TSV · DOCX · PDF*</p><input id="docFile" type="file" class="control" accept=".txt,.md,.markdown,.html,.htm,.json,.csv,.tsv,.docx,.pdf,text/*,application/json,application/pdf"></div><div class="row stack" style="margin-top:12px"><div class="grow"><label>Titel</label><input id="docTitle" class="control" placeholder="optional"></div><div class="grow"><label>Tags (Komma)</label><input id="docTags" class="control" placeholder="nvidia,cuda"></div><div style="width:110px"><label>Trust 0–1</label><input id="docTrust" class="control" type="number" min="0" max="1" step="0.05" value="1"></div></div><button class="btn" id="uploadBtn">Dokument lernen</button><p class="muted small">* PDF wird serverseitig über <span class="mono">pdftotext</span> extrahiert; das Release-Containerimage enthält das Tool.</p></div>
|
||
<div class="card"><h2>Text einspeisen</h2><div class="field"><label>Titel / Kontext</label><input id="textTitle" class="control" placeholder="z. B. Interne Notiz: NVIDIA Infrastruktur"></div><div class="field"><label>Text</label><textarea id="textInput" class="control" placeholder="Hier Wissen, Notizen, Protokolle oder kopierten Dokumenttext einfügen …"></textarea></div><div class="row stack"><div class="grow"><label>Quelle/URI (optional)</label><input id="textURI" class="control" placeholder="https://… oder interne://…"></div><div style="width:110px"><label>Trust 0–1</label><input id="textTrust" class="control" type="number" min="0" max="1" step="0.05" value="1"></div></div><button class="btn" id="textLearnBtn">Text lernen</button></div>
|
||
</div>
|
||
<div class="card" style="margin-top:14px"><div class="itemTitle"><h2>Quellenbibliothek</h2><button class="btn ghost" id="sourcesReloadBtn">Neu laden</button></div><div id="sourcesList" class="sourceGrid"></div></div>
|
||
</section>
|
||
|
||
<section class="view" id="view-research">
|
||
<div class="grid two">
|
||
<div class="card"><h2>SearXNG Research</h2><div class="row stack"><div class="grow"><label>Suchanfrage</label><input id="researchQuery" class="control" placeholder="z. B. NVIDIA Blackwell Architektur offizielle Dokumentation"></div><div style="width:130px"><label>Max Ergebnisse</label><input id="researchMax" class="control" type="number" min="1" max="50" value="8"></div></div><div class="checks"><label><input type="checkbox" id="researchLearn"> Ergebnisse lernen</label><label><input type="checkbox" id="researchFetch" checked> Seiten abrufen</label></div><div class="row" style="margin-top:12px"><button class="btn" id="researchBtn">Suchen</button><button class="btn secondary" id="researchTestBtn">Verbindung testen</button></div><div id="researchResults" class="list" style="margin-top:14px"></div></div>
|
||
<div class="card"><h2>Research-Konfiguration</h2><div class="checks"><label><input type="checkbox" id="rcEnabled"> Research aktiv</label><label><input type="checkbox" id="rcSearx"> SearXNG aktiv</label><label><input type="checkbox" id="rcGoal"> Goal-Research</label></div><div class="field" style="margin-top:12px"><label>SearXNG Base URL</label><input id="rcBase" class="control mono" placeholder="http://searxng:8080"></div><div class="row stack"><div class="grow"><label>Sprache</label><input id="rcLang" class="control" value="de-DE"></div><div class="grow"><label>Kategorien</label><input id="rcCats" class="control" value="general,science,it"></div></div><div class="row stack"><div class="grow"><label>Queries / Goal-Cycle</label><input id="rcQueries" class="control" type="number" min="1" max="20"></div><div class="grow"><label>Seiten / Cycle</label><input id="rcPages" class="control" type="number" min="0" max="50"></div><div class="grow"><label>Goal-Intervall (min)</label><input id="rcInterval" class="control" type="number" min="1"></div></div><div class="field"><label>Authorization-Header (optional, wird nicht zurückgelesen)</label><input id="rcAuth" class="control mono" type="password" placeholder="Bearer …"></div><div class="checks"><label><input type="checkbox" id="rcFetch"> Webseiten nachladen</label><label><input type="checkbox" id="rcAuto"> Autonomie aktiv</label><label><input type="checkbox" id="rcImmediate"> neue Goals sofort einplanen</label></div><button class="btn" id="researchSaveBtn" style="margin-top:12px">Research speichern</button><p class="muted small">Private Ziel-URLs sind für den Web-Fetch standardmäßig blockiert (SSRF-Schutz). Die lokale SearXNG-Adresse ist davon nicht betroffen, weil sie nur als Such-API angesprochen wird. PDF/DOCX/TXT/MD/CSV/JSON-Dateitreffer werden automatisch über die Dokument-Pipeline extrahiert, gechunkt und als quellengebundene Evidence verarbeitet.</p></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="view" id="view-goals">
|
||
<div class="grid two"><div class="card"><h2>Neues Ziel</h2><div class="field"><label>Titel</label><input id="goalTitle" class="control" placeholder="NVIDIA"></div><div class="field"><label>Beschreibung</label><textarea id="goalDesc" class="control" placeholder="Welche Wissenslücke soll das System autonom schließen?"></textarea></div><div class="field"><label>Messbares Ziel</label><input id="goalTarget" class="control" placeholder="z. B. 100 hochwertige, quellengebundene Wissenseinträge"></div><div class="row stack"><div class="grow"><label>Priorität 1–100</label><input id="goalPriority" class="control" type="number" min="1" max="100" value="50"></div><div class="grow"><label>Intervall (Minuten)</label><input id="goalInterval" class="control" type="number" min="1" value="10"></div></div><button class="btn" id="goalCreateBtn">Ziel anlegen</button></div><div class="card"><h2>So arbeitet ein Goal</h2><div class="pre">FÄLLIG
|
||
↓
|
||
SEARCH PLAN (Actor)
|
||
↓
|
||
SEARXNG → Web Evidence
|
||
↓
|
||
Chunk + Embed + Dedup
|
||
↓
|
||
Recall vorhandenes + neues Wissen
|
||
↓
|
||
Predict → Evaluate → Learn
|
||
↓
|
||
NextCycleAt / Backoff</div><p class="muted small">Ein neues Ziel wird bei aktivierter Autonomie sofort fällig. Danach besitzt jedes Goal sein eigenes Intervall und Fehler-Backoff.</p></div></div>
|
||
<div class="card" style="margin-top:14px"><div class="itemTitle"><h2>Ziele</h2><button class="btn secondary" id="runAutonomyBtn">Alle fälligen jetzt</button></div><div class="goalGrid" id="goalsList"></div></div>
|
||
<div class="card researchLive" id="goalResearchLive">
|
||
<div class="liveHead"><div class="liveTitle"><span class="livePulse" id="livePulse"></span><div><h2 style="margin:0" id="liveGoalTitle">Live Research</h2><div class="muted small" id="liveRunMeta">Noch kein Research-Lauf ausgewählt.</div></div></div><div class="row"><button class="btn ghost" id="liveRefreshBtn">Aktualisieren</button><button class="btn ghost" id="liveCloseBtn">Schließen</button></div></div>
|
||
<div class="liveStats"><div class="liveStat"><b id="lsResults">0</b><span>Treffer</span></div><div class="liveStat"><b id="lsDownloads">0</b><span>Downloads</span></div><div class="liveStat"><b id="lsClaims">0</b><span>Claims</span></div><div class="liveStat"><b id="lsNew">0</b><span>Neu gelernt</span></div><div class="liveStat"><b id="lsDup">0</b><span>Duplikate</span></div><div class="liveStat"><b id="lsConfirm">0</b><span>Bestätigt</span></div></div>
|
||
<div><label>Aktuelle / geplante Suchqueries</label><div class="queryChips" id="liveQueries"><span class="muted small">–</span></div></div>
|
||
<div class="eventBoard">
|
||
<div class="eventLane"><div class="eventLaneHead"><b>⌕ Queries</b><span class="pill" id="lcQueries">0</span></div><div class="eventLaneBody" id="laneQueries"></div></div>
|
||
<div class="eventLane"><div class="eventLaneHead"><b>↗ Gefundene URLs</b><span class="pill" id="lcResults">0</span></div><div class="eventLaneBody" id="laneResults"></div></div>
|
||
<div class="eventLane"><div class="eventLaneHead"><b>⇣ Downloads / Quellen</b><span class="pill" id="lcDownloads">0</span></div><div class="eventLaneBody" id="laneDownloads"></div></div>
|
||
<div class="eventLane"><div class="eventLaneHead"><b>◈ Claims / Evidenz</b><span class="pill" id="lcClaims">0</span></div><div class="eventLaneBody" id="laneClaims"></div></div>
|
||
<div class="eventLane"><div class="eventLaneHead"><b>✓ Duplikate / Bestätigung</b><span class="pill" id="lcDedupe">0</span></div><div class="eventLaneBody" id="laneDedupe"></div></div>
|
||
<div class="eventLane"><div class="eventLaneHead"><b>! Verworfen / Fehler</b><span class="pill" id="lcRejected">0</span></div><div class="eventLaneBody" id="laneRejected"></div></div>
|
||
</div>
|
||
<h3>Letzte Research-Läufe</h3><div class="researchHistory" id="researchHistory"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="view" id="view-chat">
|
||
<div class="card"><h2>Chat gegen das lernende System</h2><div class="row stack"><div class="grow"><label>Session</label><input id="chatSession" class="control" value="web-demo"></div><div class="grow"><label>Provider</label><select id="chatProvider" class="control"><option value="">Auto</option><option value="ollama">Ollama</option><option value="openai">OpenAI</option></select></div></div><div class="field"><label>Nachricht</label><textarea id="chatPrompt" class="control" style="min-height:180px" placeholder="Frage stellen …"></textarea></div><div class="row"><button class="btn" id="chatBtn">Senden & lernen</button><button class="btn danger" id="chatAbortBtn" disabled>Abbrechen</button><span class="muted small" id="chatElapsed"></span></div><div id="chatOutput" class="pre" style="margin-top:14px;min-height:80px"></div></div>
|
||
</section>
|
||
|
||
<section class="view" id="view-models">
|
||
<div class="grid two"><div class="card"><h2>Modelle & Routing</h2><p class="muted small">Hier siehst du den aktiven Routing-Block. Für feine Änderungen kannst du den JSON-Editor verwenden.</p><div id="modelCards" class="list"></div><button class="btn secondary" id="modelHealthBtn" style="margin-top:12px">Ollama-Verbindungen prüfen</button><div id="modelHealth" class="pre" style="margin-top:10px"></div></div><div class="card"><h2>Routing JSON</h2><textarea id="modelJSON" class="control mono" style="min-height:410px"></textarea><button class="btn" id="modelSaveBtn">Routing speichern</button></div></div>
|
||
</section>
|
||
|
||
<section class="view" id="view-ops">
|
||
<div class="grid three"><div class="card"><h2>Index</h2><div id="opsIndex" class="list"></div><div class="row"><button class="btn secondary" id="pqBuildBtn">PQ rebuild</button><button class="btn ghost" id="indexMergeBtn">HNSW merge</button></div></div><div class="card"><h2>Storage</h2><div id="opsStorage" class="list"></div><div class="row"><button class="btn secondary" id="checkpointBtn">Checkpoint</button><button class="btn ghost" id="compactBtn">Kompaktieren</button></div></div><div class="card"><h2>Observability</h2><div id="opsObs" class="list"></div><div class="field"><label>Prometheus</label><div class="pre mono">GET /metrics\nAuthorization: Bearer <METRICS_TOKEN></div></div></div></div>
|
||
<div class="card" style="margin-top:14px"><h2>Gesamtkonfiguration</h2><textarea id="configJSON" class="control mono" style="min-height:420px"></textarea><button class="btn" id="configSaveBtn">Konfiguration speichern</button></div>
|
||
</section>
|
||
</main>
|
||
</div>
|
||
<nav class="mobileNav" id="mobileNav"><button class="active" data-view="dashboard">Home</button><button data-view="knowledge">Graph</button><button data-view="sources">Import</button><button data-view="research">Web</button><button data-view="goals">Goals</button><button data-view="chat">Chat</button><button data-view="ops">Ops</button></nav>
|
||
<div class="toast" id="toast"></div>
|
||
<script>
|
||
'use strict';
|
||
const $=id=>document.getElementById(id), $$=q=>[...document.querySelectorAll(q)];
|
||
let admin=sessionStorage.getItem('neuroforge_admin_token')||'', currentView='dashboard', chatAbort=null, graphData={nodes:[],edges:[]}, graphPos=new Map(), selectedMemory='', graphLayoutReady=false, researchWatch={goalId:'',goalTitle:'',runId:'',after:0,events:[],run:null,timer:null};
|
||
const viewMeta={dashboard:['Übersicht','Was das System weiß, lernt und gerade tut.'],knowledge:['Wissensraum','Interaktiver Graph mit Level-of-Detail und echter Recall-Erklärung.'],sources:['Quellen & Import','Dokumente und Text werden zu nachvollziehbarer Evidence.'],research:['Research','SearXNG suchen, Webseiten abrufen und Quellen kontrolliert lernen.'],goals:['Ziele & Autonomie','Per-Goal Scheduler, Research und Fehler-Backoff.'],chat:['Chat','Interaktiv mit Recall, Lernen und Abbruch.'],models:['Modelle & Routing','Actor, Embeddings und Ollama-Nodes.'],ops:['Betrieb','Index, Storage, Observability und Rohkonfiguration.']};
|
||
function toast(msg,bad=false){let t=$('toast');t.textContent=msg;t.className='toast show'+(bad?' bad':'');clearTimeout(t._timer);t._timer=setTimeout(()=>t.className='toast',4500)}
|
||
function esc(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
|
||
function safeHref(raw){try{let u=new URL(String(raw||''),location.origin);return (u.protocol==='http:'||u.protocol==='https:')?u.href:'#'}catch{return '#'}}
|
||
function fmtN(n){return Number(n||0).toLocaleString('de-DE')}function fmtBytes(n){n=Number(n||0);if(n<1024)return n+' B';if(n<1048576)return(n/1024).toFixed(1)+' KiB';if(n<1073741824)return(n/1048576).toFixed(1)+' MiB';return(n/1073741824).toFixed(2)+' GiB'}
|
||
function normalizeStatus(s){s=s&&typeof s==='object'?s:{};let st=s.stats&&typeof s.stats==='object'?s.stats:{};return {...s,...st}}
|
||
function when(d){if(!d)return'–';let x=new Date(d),ms=x-Date.now(),a=Math.abs(ms),m=Math.round(a/60000);if(a<60000)return ms>0?'gleich':'gerade';if(m<60)return(ms>0?'in ':'vor ')+m+' min';let h=Math.round(m/60);return(ms>0?'in ':'vor ')+h+' h'}
|
||
function validHeaderToken(value){return typeof value==='string'&&/^[\x21-\x7E]+$/.test(value)}
|
||
function tokenHeaders(json=true){if(!admin)throw new Error('Admin-Token fehlt.');if(!validHeaderToken(admin))throw new Error('Admin-Token enthält Leerzeichen/Unicode. Bitte echten ASCII-Token neu eingeben.');let h={'X-Admin-Token':admin};if(json)h['Content-Type']='application/json';return h}
|
||
async function req(path,opt={}){let o={...opt,headers:{...(opt.headers||{}),...tokenHeaders(opt.json!==false)}};let r=await fetch(path,o),txt=await r.text();let data=null;try{data=txt?JSON.parse(txt):null}catch{data=txt}if(!r.ok)throw new Error((data&&data.error)||data||`HTTP ${r.status}`);return data}
|
||
async function setToken(){let v=prompt('Admin-Token',admin||'');if(v===null)return;v=v.trim();if(!v)return;admin=v;sessionStorage.setItem('neuroforge_admin_token',v);await refreshAll()}
|
||
function switchView(v){currentView=v;$$('.view').forEach(x=>x.classList.toggle('active',x.id==='view-'+v));$$('[data-view]').forEach(x=>x.classList.toggle('active',x.dataset.view===v));$('pageTitle').textContent=viewMeta[v][0];$('pageSub').textContent=viewMeta[v][1];if(v!=='goals')stopResearchPolling();if(v==='knowledge')loadKnowledgeGraph();if(v==='sources')loadSources();if(v==='research')loadResearchConfig();if(v==='goals'){loadGoals();if(researchWatch.goalId)startResearchPolling()}if(v==='models')loadModels();if(v==='ops')loadOps()}
|
||
$$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset.view)));$('tokenBtn').addEventListener('click',setToken);$('refreshBtn').addEventListener('click',refreshAll);
|
||
|
||
async function refreshAll(){if(!admin){$('sideStatus').textContent='Admin-Token setzen';return}try{let [raw,k,g,src]=await Promise.all([req('/admin/api/status'),req('/admin/api/knowledge/summary'),req('/api/v1/goals'),req('/api/v1/sources?limit=8')]),s=normalizeStatus(raw);src=Array.isArray(src)?src:[];g=Array.isArray(g)?g:[];$('sideDot').className='dot ok';$('sideStatus').textContent='bereit · rev '+fmtN(s.revision);$('mMem').textContent=fmtN(s.memories);$('mSyn').textContent=fmtN(s.synapses);$('mSources').textContent=fmtN(k?.sources||src.length);let active=g.filter(x=>x.status==='active');$('mGoals').textContent=active.length;$('mNextGoal').textContent=active.length?('nächster '+when(active.map(x=>x.next_cycle_at).filter(Boolean).sort()[0])):'keine aktiven Ziele';$('mTypes').textContent=Object.entries(k?.by_type||{}).map(([a,b])=>a+' '+b).join(' · ');renderSystemState(s);renderEvents(k?.recent_events||[],'dashEvents');renderDashboardGoals(active.slice(0,5));if(currentView==='ops')loadOps()}catch(e){$('sideDot').className='dot bad';$('sideStatus').textContent='Fehler';toast(e.message,true)}}
|
||
function renderSystemState(s){let o=s.observability||{},m=s.maintenance||{};$('systemState').innerHTML=[['Index',`${s.index_mode||o.index_mode||'–'} · HNSW ${fmtN(s.hnsw_nodes)} · PQ ${fmtN(s.disk_pq_items)}`],['Jobs',fmtN(s.pending_jobs)+' ausstehend'],['Cluster',s.cluster?.enabled?(s.cluster.role+' · term '+s.cluster.term):'deaktiviert'],['Letzter Autonomie-Lauf',m.last_autonomy_run?new Date(m.last_autonomy_run).toLocaleString():'–'],['Letzter Fehler',m.last_error||'keiner']].map(([a,b])=>`<div class="item"><div class="itemTitle"><b>${esc(a)}</b><span class="muted small">${esc(b)}</span></div></div>`).join('')}
|
||
function renderEvents(xs,id){xs=Array.isArray(xs)?xs:[];$(id).innerHTML=xs.length?xs.map(e=>`<div class="item"><div class="itemTitle"><b>${esc(e.type)}</b><span class="pill">${esc(e.actor||'system')}</span></div><p>${esc(e.summary||'')}</p><div class="meta"><span>${new Date(e.created_at).toLocaleString()}</span>${e.reason?`<span>${esc(e.reason)}</span>`:''}</div></div>`).join(''):'<p class="muted">Noch keine Lernereignisse.</p>'}
|
||
function renderDashboardGoals(xs){$('dashGoals').innerHTML=xs.length?xs.map(g=>`<div class="item"><div class="itemTitle"><b>${esc(g.title)}</b><span class="pill ${g.auto_run?'good':''}">${g.auto_run?'AUTO':'MANUELL'}</span></div><div class="progress" style="margin:9px 0"><span style="width:${Math.round((g.progress||0)*100)}%"></span></div><div class="meta"><span>${Math.round((g.progress||0)*100)}%</span><span>${g.research_enabled?'Research an':'Research aus'}</span><span>${g.next_cycle_at?when(g.next_cycle_at):'nicht geplant'}</span></div></div>`).join(''):'<p class="muted">Keine aktiven Ziele.</p>'}
|
||
|
||
// ---- Knowledge graph with client-side LOD ----
|
||
const canvas=$('knowledgeCanvas'),ctx=canvas.getContext('2d'),cam={x:0,y:0,z:1},pointer={down:false,x:0,y:0,moved:false},typeColors={semantic:'#7aa7ff',episodic:'#65d6b5',procedural:'#f7c36d',working:'#bd8cff'};
|
||
function hash(s){let h=2166136261;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619)}return h>>>0}function rand01(s){return(hash(s)%100000)/100000}
|
||
function layoutGraph(){graphPos.clear();let types=[...new Set(graphData.nodes.map(n=>n.memory_type||'other'))],centers={};types.forEach((t,i)=>{let a=i/types.length*Math.PI*2;centers[t]={x:Math.cos(a)*260,y:Math.sin(a)*210}});for(let n of graphData.nodes){let c=centers[n.memory_type||'other']||{x:0,y:0},a=rand01(n.id)*Math.PI*2,r=40+rand01(n.id+'r')*170;graphPos.set(n.id,{x:c.x+Math.cos(a)*r,y:c.y+Math.sin(a)*r,vx:0,vy:0,n})}for(let it=0;it<110;it++){for(let e of graphData.edges){let a=graphPos.get(e.a),b=graphPos.get(e.b);if(!a||!b)continue;let dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1,target=65+70/(1+Math.abs(Number(e.weight||0))),f=(d-target)*.0035,fx=dx/d*f,fy=dy/d*f;a.vx+=fx;a.vy+=fy;b.vx-=fx;b.vy-=fy}for(let p of graphPos.values()){let c=centers[p.n.memory_type||'other']||{x:0,y:0};p.vx+=(c.x-p.x)*.0007;p.vy+=(c.y-p.y)*.0007;p.vx*=.86;p.vy*=.86;p.x+=p.vx;p.y+=p.vy}}graphLayoutReady=true;fitGraph();drawGraph()}
|
||
function resizeCanvas(){let r=canvas.getBoundingClientRect(),dpr=devicePixelRatio||1;canvas.width=Math.max(1,Math.floor(r.width*dpr));canvas.height=Math.max(1,Math.floor(r.height*dpr));ctx.setTransform(dpr,0,0,dpr,0,0);drawGraph()}new ResizeObserver(resizeCanvas).observe(canvas);
|
||
function worldToScreen(p){let r=canvas.getBoundingClientRect();return{x:r.width/2+cam.x+p.x*cam.z,y:r.height/2+cam.y+p.y*cam.z}}function screenToWorld(x,y){let r=canvas.getBoundingClientRect();return{x:(x-r.width/2-cam.x)/cam.z,y:(y-r.height/2-cam.y)/cam.z}}
|
||
function lod(){return cam.z<.62?0:cam.z<1.45?1:2}function drawGraph(){let r=canvas.getBoundingClientRect();ctx.clearRect(0,0,r.width,r.height);let L=lod();$('lodBadge').textContent='LOD · '+(['Übersicht','Netz','Detail'][L]);$('graphZoom').textContent=Math.round(cam.z*100)+'%';if(!graphLayoutReady)return;if(L===0){drawClusters();return}let edges=graphData.edges.slice(0,L===1?Math.min(graphData.edges.length,graphData.nodes.length*2):graphData.edges.length);for(let e of edges){let a=graphPos.get(e.a),b=graphPos.get(e.b);if(!a||!b)continue;let A=worldToScreen(a),B=worldToScreen(b),w=Math.abs(Number(e.weight||0));ctx.strokeStyle=Number(e.weight||0)<0?'rgba(255,124,141,.28)':'rgba(117,153,207,'+(L===2?.32:.18)+')';ctx.lineWidth=Math.min(4,.45+w*.8);ctx.beginPath();ctx.moveTo(A.x,A.y);ctx.lineTo(B.x,B.y);ctx.stroke()}for(let p of graphPos.values()){let s=worldToScreen(p),sel=p.n.id===selectedMemory,rr=(sel?8:5)*(L===2?1.15:1);ctx.fillStyle=typeColors[p.n.memory_type]||'#bd8cff';ctx.beginPath();ctx.arc(s.x,s.y,rr,0,Math.PI*2);ctx.fill();if(sel){ctx.strokeStyle='#fff';ctx.lineWidth=2;ctx.stroke()}let important=(p.n.salience||0)>1.15||(p.n.access_count||0)>4;if(sel||(L===2&&important)){ctx.fillStyle='#dce7f7';ctx.font='11px system-ui';let txt=(p.n.truth_key||p.n.text||p.n.kind||p.n.id).slice(0,34);ctx.fillText(txt,s.x+10,s.y+4)}}}
|
||
function drawClusters(){let groups={};for(let p of graphPos.values()){let t=p.n.memory_type||'other';(groups[t]??=[]).push(p)}for(let[t,xs]of Object.entries(groups)){let x=xs.reduce((a,p)=>a+p.x,0)/xs.length,y=xs.reduce((a,p)=>a+p.y,0)/xs.length,s=worldToScreen({x,y}),rad=Math.min(58,20+Math.sqrt(xs.length)*6);ctx.fillStyle=(typeColors[t]||'#bd8cff')+'44';ctx.strokeStyle=typeColors[t]||'#bd8cff';ctx.lineWidth=2;ctx.beginPath();ctx.arc(s.x,s.y,rad,0,Math.PI*2);ctx.fill();ctx.stroke();ctx.fillStyle='#eef3fb';ctx.font='600 13px system-ui';ctx.textAlign='center';ctx.fillText(t,s.x,s.y-2);ctx.fillStyle='#a9b7cb';ctx.font='11px system-ui';ctx.fillText(xs.length+' nodes',s.x,s.y+15)}ctx.textAlign='left'}
|
||
function fitGraph(){if(!graphPos.size)return;let xs=[...graphPos.values()].map(p=>p.x),ys=[...graphPos.values()].map(p=>p.y),w=Math.max(...xs)-Math.min(...xs)+140,h=Math.max(...ys)-Math.min(...ys)+140,r=canvas.getBoundingClientRect();cam.z=Math.max(.25,Math.min(1.15,Math.min(r.width/w,r.height/h)));cam.x=cam.y=0;drawGraph()}
|
||
canvas.addEventListener('wheel',e=>{e.preventDefault();let before=screenToWorld(e.offsetX,e.offsetY),factor=Math.exp(-e.deltaY*.0012);cam.z=Math.max(.2,Math.min(5,cam.z*factor));let after=worldToScreen(before);cam.x+=e.offsetX-after.x;cam.y+=e.offsetY-after.y;drawGraph()},{passive:false});canvas.addEventListener('pointerdown',e=>{pointer.down=true;pointer.x=e.clientX;pointer.y=e.clientY;pointer.moved=false;canvas.setPointerCapture(e.pointerId)});canvas.addEventListener('pointermove',e=>{if(!pointer.down)return;let dx=e.clientX-pointer.x,dy=e.clientY-pointer.y;if(Math.hypot(dx,dy)>2)pointer.moved=true;cam.x+=dx;cam.y+=dy;pointer.x=e.clientX;pointer.y=e.clientY;drawGraph()});canvas.addEventListener('pointerup',e=>{pointer.down=false;if(pointer.moved)return;let rect=canvas.getBoundingClientRect(),x=e.clientX-rect.left,y=e.clientY-rect.top;if(lod()===0){cam.z=.8;drawGraph();return}let best=null,bd=15;for(let p of graphPos.values()){let s=worldToScreen(p),d=Math.hypot(s.x-x,s.y-y);if(d<bd){best=p;bd=d}}if(best)openMemory(best.n.id)});$('zoomInBtn').addEventListener('click',()=>{cam.z=Math.min(5,cam.z*1.3);drawGraph()});$('zoomOutBtn').addEventListener('click',()=>{cam.z=Math.max(.2,cam.z/1.3);drawGraph()});$('fitBtn').addEventListener('click',fitGraph);
|
||
async function loadKnowledgeGraph(center=''){try{let d=await req('/admin/api/knowledge/graph?depth=3&max_nodes=600'+(center?'¢er='+encodeURIComponent(center):''));graphData={...(d||{}),nodes:Array.isArray(d?.nodes)?d.nodes:[],edges:Array.isArray(d?.edges)?d.edges:[]};let tn=Number(d?.total_memories??graphData.nodes.length),te=Number(d?.total_synapses??graphData.edges.length),ns=fmtN(graphData.nodes.length)+(tn>graphData.nodes.length?' / '+fmtN(tn):''),es=fmtN(graphData.edges.length)+(te>graphData.edges.length?' / '+fmtN(te):'');$('graphCount').textContent=ns+' Knoten · '+es+' Kanten';if(tn>0&&graphData.nodes.length===0)toast('Memory-Katalog enthält '+fmtN(tn)+' Einträge, aber der Graph konnte keine kanonischen IDs auflösen.',true);layoutGraph()}catch(e){graphData={nodes:[],edges:[]};graphLayoutReady=false;$('graphCount').textContent='0 Knoten · 0 Kanten';drawGraph();toast(e.message,true)}}$('graphReloadBtn').addEventListener('click',()=>loadKnowledgeGraph(selectedMemory));
|
||
$('knowledgeSearchBtn').addEventListener('click',searchKnowledge);$('knowledgeQuery').addEventListener('keydown',e=>{if(e.key==='Enter')searchKnowledge()});async function searchKnowledge(){let q=$('knowledgeQuery').value.trim();if(!q)return;try{let d=await req('/admin/api/knowledge/search',{method:'POST',body:JSON.stringify({text:q,k:16})});$('knowledgeHits').innerHTML=(d.hits||[]).map(h=>`<div class="item click" data-mid="${esc(h.memory.id)}"><div class="itemTitle"><b>${esc(h.memory.text.slice(0,120))}</b><span class="pill">${Number(h.score||0).toFixed(3)}</span></div><div class="meta"><span>${esc(h.candidate_source||'')}</span><span>sim ${Number(h.similarity||0).toFixed(3)}</span><span>base ${Number(h.base_score||0).toFixed(3)}</span><span>graph +${Number(h.graph_boost||0).toFixed(3)}</span></div></div>`).join('')||'<p class="muted">Keine Treffer.</p>';$$('#knowledgeHits [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));if(d.hits?.[0]){selectedMemory=d.hits[0].memory.id;await loadKnowledgeGraph(selectedMemory);await openMemory(selectedMemory,false)}}catch(e){toast(e.message,true)}}
|
||
async function openMemory(id,recenter=true){selectedMemory=id;try{let d=await req('/admin/api/knowledge/memory/'+encodeURIComponent(id)),m=d.memory||{},p=m.provenance||{};$('knowledgeInspector').innerHTML=`<div class="meta"><span class="pill">${esc(m.memory_type)}</span><span class="pill">${esc(m.kind)}</span><span class="pill ${m.status==='active'?'good':m.status==='conflicted'?'warn':''}">${esc(m.status||'active')}</span></div><h3>${esc(m.truth_key||p.source_title||m.id)}</h3><div style="white-space:pre-wrap;line-height:1.5;font-size:13px">${esc(m.text)}</div><h3>Qualität</h3><div class="kv"><div>Confidence</div><div>${Number(m.confidence||0).toFixed(3)}</div><div>Salience</div><div>${Number(m.salience||0).toFixed(3)}</div><div>Reward</div><div>${Number(m.reward||0).toFixed(3)}</div><div>Evidenzquellen</div><div>${fmtN(m.evidence_count||m.evidence_source_ids?.length||0)}</div><div>Aufrufe</div><div>${fmtN(m.access_count)}</div></div><h3>Provenance</h3><div class="kv"><div>Quelle</div><div>${esc(p.source||'legacy/unknown')}</div><div>Source-ID</div><div class="mono">${esc(p.source_id||'–')}</div><div>Titel</div><div>${esc(p.source_title||'–')}</div><div>URI</div><div>${p.source_uri?`<a href="${esc(safeHref(p.source_uri))}" target="_blank" rel="noopener" style="color:var(--accent)">${esc(p.source_uri)}</a>`:'–'}</div><div>Chunk</div><div>${p.chunk_index||'–'} / ${p.chunk_count||'–'}</div><div>Embedding</div><div>${esc([p.embedding_model,p.embedding_node_id].filter(Boolean).join(' · ')||'–')}</div><div>Generierung</div><div>${esc([p.generation_model,p.generation_node_id].filter(Boolean).join(' · ')||'–')}</div></div><h3>Stärkste Synapsen</h3>${(d.edges||[]).slice(0,12).map(e=>`<div class="item click" data-mid="${esc(e.a===id?e.b:e.a)}"><div class="itemTitle"><span class="mono small">${esc(e.a===id?e.b:e.a)}</span><span class="pill">w ${Number(e.weight).toFixed(2)}</span></div></div>`).join('')||'<p class="muted">Keine Synapsen.</p>'}<h3>Timeline</h3>${(d.events||[]).map(e=>`<div class="item"><b>${esc(e.type)}</b><p>${esc(e.summary)}</p><div class="meta">${new Date(e.created_at).toLocaleString()}</div></div>`).join('')||'<p class="muted">Keine Events.</p>'}`;$$('#knowledgeInspector [data-mid]').forEach(x=>x.addEventListener('click',()=>openMemory(x.dataset.mid)));drawGraph();if(recenter)loadKnowledgeGraph(id)}catch(e){toast(e.message,true)}}
|
||
|
||
// ---- Sources & ingestion ----
|
||
async function loadSources(){try{let xs=await req('/api/v1/sources?limit=100');xs=Array.isArray(xs)?xs:[];$('sourcesList').innerHTML=xs.length?xs.map(s=>`<div class="item"><div class="itemTitle"><div class="row"><span class="sourceIcon">${s.type==='document'?'▤':s.type==='web'?'⌁':'T'}</span><div><b>${esc(s.title)}</b><div class="meta"><span>${esc(s.type)}</span><span>${s.chunk_count||0} chunks</span><span>${fmtBytes(s.bytes)}</span></div></div></div><span class="pill ${s.status==='ready'?'good':s.status==='error'?'bad':'warn'}">${esc(s.status)}</span></div>${s.uri?`<p class="mono small">${esc(s.uri)}</p>`:''}${s.error?`<p class="error small">${esc(s.error)}</p>`:''}</div>`).join(''):'<p class="muted">Noch keine externen Quellen.</p>';$('mSources').textContent=fmtN(xs.length)}catch(e){toast(e.message,true)}}$('sourcesReloadBtn').addEventListener('click',loadSources);
|
||
const dz=$('dropZone'),fi=$('docFile');['dragenter','dragover'].forEach(n=>dz.addEventListener(n,e=>{e.preventDefault();dz.classList.add('drag')}));['dragleave','drop'].forEach(n=>dz.addEventListener(n,e=>{e.preventDefault();dz.classList.remove('drag')}));dz.addEventListener('drop',e=>{if(e.dataTransfer.files.length)fi.files=e.dataTransfer.files});$('uploadBtn').addEventListener('click',async()=>{let f=fi.files[0];if(!f)return toast('Bitte eine Datei auswählen.',true);let fd=new FormData();fd.append('file',f);fd.append('title',$('docTitle').value);fd.append('tags',$('docTags').value);fd.append('trust',$('docTrust').value);try{let r=await fetch('/api/v1/ingest/document',{method:'POST',headers:tokenHeaders(false),body:fd}),txt=await r.text(),d;try{d=JSON.parse(txt)}catch{d=txt}if(!r.ok)throw new Error(d.error||d);toast(`Dokument gelernt: ${d.chunks} Chunks, ${d.duplicates} Duplikate`);fi.value='';loadSources();refreshAll()}catch(e){toast(e.message,true)}});
|
||
$('textLearnBtn').addEventListener('click',async()=>{try{let d=await req('/api/v1/ingest/text',{method:'POST',body:JSON.stringify({title:$('textTitle').value||'Manual text',text:$('textInput').value,source_uri:$('textURI').value,trust:Number($('textTrust').value||1),source_type:'text',memory_type:'semantic'})});toast(`Text gelernt: ${d.chunks} Chunks`);$('textInput').value='';loadSources();refreshAll()}catch(e){toast(e.message,true)}});
|
||
|
||
// ---- Research ----
|
||
async function loadResearchConfig(){try{let d=await req('/admin/api/research'),r=d.research||{},s=r.searxng||{},w=r.web_fetch||{},g=r.goal||{},a=d.autonomy||{};$('rcEnabled').checked=!!r.enabled;$('rcSearx').checked=!!s.enabled;$('rcGoal').checked=!!g.enabled;$('rcBase').value=s.base_url||'';$('rcLang').value=s.language||'de-DE';$('rcCats').value=s.categories||'general';$('rcQueries').value=g.max_queries_per_cycle||2;$('rcPages').value=g.max_pages_per_cycle??4;$('rcFetch').checked=!!w.enabled;$('rcAuto').checked=!!a.enabled;$('rcImmediate').checked=!!a.run_on_goal_create;$('rcInterval').value=a.default_goal_interval_minutes||10;$('rcAuth').placeholder=d.searxng_auth_configured?'konfiguriert – leer lassen zum Behalten':'Bearer …'}catch(e){toast(e.message,true)}}
|
||
$('researchSaveBtn').addEventListener('click',async()=>{try{let current=await req('/admin/api/research'),r=current.research||{};r.enabled=$('rcEnabled').checked;r.searxng={...(r.searxng||{}),enabled:$('rcSearx').checked,base_url:$('rcBase').value.trim(),language:$('rcLang').value.trim(),categories:$('rcCats').value.trim()};r.web_fetch={...(r.web_fetch||{}),enabled:$('rcFetch').checked};r.goal={...(r.goal||{}),enabled:$('rcGoal').checked,max_queries_per_cycle:Number($('rcQueries').value||2),max_pages_per_cycle:Number($('rcPages').value||0)};let body={research:r,ingestion:current.ingestion,autonomy:{enabled:$('rcAuto').checked,run_on_goal_create:$('rcImmediate').checked,default_goal_interval_minutes:Number($('rcInterval').value||10)}};if($('rcAuth').value)body.searxng_auth_header=$('rcAuth').value;await req('/admin/api/research',{method:'PUT',body:JSON.stringify(body)});$('rcAuth').value='';toast('Research-Konfiguration gespeichert.')}catch(e){toast(e.message,true)}});
|
||
$('researchBtn').addEventListener('click',()=>runResearch(false));$('researchTestBtn').addEventListener('click',()=>runResearch(true));
|
||
function researchKind(x){let file=!!(x.filename||x.mimetype||String(x.template||'').toLowerCase().includes('file')||/\.(pdf|docx|txt|md|markdown|csv|tsv|json|ya?ml)(?:$|[?#])/i.test(x.url||''));return file?'DOKUMENT':'WEB'}
|
||
async function runResearch(test){let q=$('researchQuery').value.trim()||(test?'NVIDIA CUDA':'');if(!q)return toast('Suchanfrage fehlt.',true);$('researchResults').innerHTML='<p class="muted">SearXNG sucht …</p>';try{let d=test?await req('/admin/api/research/test',{method:'POST',body:JSON.stringify({query:q})}):await req('/api/v1/research',{method:'POST',body:JSON.stringify({query:q,learn:$('researchLearn').checked,fetch_pages:$('researchFetch').checked,max_results:Number($('researchMax').value||8)})});$('researchResults').innerHTML=(d.results||[]).map(x=>`<div class="item"><div class="itemTitle"><b>${esc(x.title||x.filename||x.url)}</b><span class="pill ${researchKind(x)==='DOKUMENT'?'good':''}">${researchKind(x)}</span></div><p>${esc(x.content||x.abstract||'')}</p><div class="meta"><span>${esc(x.mimetype||x.engine||((x.engines||[])[0]||'web'))}</span>${x.filename?`<span>${esc(x.filename)}</span>`:''}${x.size?`<span>${esc(x.size)}</span>`:''}<a href="${esc(safeHref(x.url))}" target="_blank" rel="noopener" style="color:var(--accent)">${esc(x.url)}</a></div></div>`).join('')||'<p class="muted">Keine Ergebnisse.</p>';if(d.ingested)toast(`${d.ingested} Evidence-Memories gelernt${d.documents_ingested?` · ${d.documents_ingested} Dokument(e) verarbeitet`:''}.`);if(d.errors?.length)toast(d.errors[0],true);if(d.ingested)loadSources()}catch(e){$('researchResults').innerHTML='<p class="error">'+esc(e.message)+'</p>';toast(e.message,true)}}
|
||
|
||
// ---- Goals + live research ----
|
||
function stopResearchPolling(){if(researchWatch.timer){clearInterval(researchWatch.timer);researchWatch.timer=null}}
|
||
function startResearchPolling(){stopResearchPolling();if(!researchWatch.goalId||currentView!=='goals')return;pollGoalResearch();researchWatch.timer=setInterval(pollGoalResearch,1200)}
|
||
function researchLane(ev){
|
||
if(['plan.started','plan.completed','plan.fallback','query.planned','search.started','search.completed'].includes(ev.type))return'queries';
|
||
if(ev.type==='search.result')return'results';
|
||
if(['download.started','download.completed','source.processing','source.ingested','source.duplicate'].includes(ev.type))return'downloads';
|
||
if(['claim.extracted','evidence.learned'].includes(ev.type))return'claims';
|
||
if(['evidence.duplicate','evidence.corroborated'].includes(ev.type))return'dedupe';
|
||
if(ev.status==='error'||ev.status==='skipped'||['source.rejected','evidence.skipped','evidence.error','search.error'].includes(ev.type))return'rejected';
|
||
return'queries';
|
||
}
|
||
function eventLabel(ev){return ({'query.planned':'Query','search.started':'SearXNG','search.completed':'SearXNG','search.result':'Treffer','download.started':'Download','download.completed':'Geladen','source.processing':'Extraktion','source.ingested':'Quelle','source.duplicate':'Quelle bekannt','claim.extracted':'Claim-Kandidat','evidence.learned':'Neu gelernt','evidence.duplicate':'Duplikat','evidence.corroborated':'Bestätigt','source.rejected':'Verworfen','evidence.skipped':'Verworfen','evidence.error':'Fehler','plan.fallback':'Fallback','run.started':'Start','run.finished':'Ende'}[ev.type]||ev.type)}
|
||
function liveEventCard(ev,terminals){let status=ev.status||'ok';if(ev.type==='download.started'&&terminals.has(ev.url))status='ok';let link=ev.url?`<a href="${esc(safeHref(ev.url))}" target="_blank" rel="noopener">${esc(ev.url)}</a>`:'';let preview=ev.preview?`<p class="preview">${esc(ev.preview)}</p>`:'';let score=ev.similarity?` · sim ${Number(ev.similarity).toFixed(3)}`:ev.score?` · score ${Number(ev.score).toFixed(3)}`:'';return `<div class="researchEvent ${esc(status)}"><div class="eventTop"><span>${esc(eventLabel(ev))}${score}</span><span>${new Date(ev.created_at).toLocaleTimeString()}</span></div><b>${esc(ev.title||ev.query||ev.message||'Event')}</b>${ev.message&&ev.message!==(ev.title||ev.query)?`<p>${esc(ev.message)}</p>`:''}${preview}${link}</div>`}
|
||
function renderResearchLive(){let run=researchWatch.run,events=researchWatch.events;if(!run){$('liveRunMeta').textContent='Für dieses Ziel existiert noch kein Research-Lauf.';$('livePulse').className='livePulse';return}
|
||
let st=run.stats||{},running=run.status==='running';$('livePulse').className='livePulse '+(running?'running':run.status==='completed_with_errors'?'warn':run.status==='failed'||run.status==='cancelled'?'error':'');let elapsed=run.started_at?Math.max(0,Math.round(((run.completed_at?new Date(run.completed_at):new Date())-new Date(run.started_at))/1000)):0;$('liveRunMeta').textContent=`${run.status} · Run ${run.id} · ${elapsed}s · letztes Event #${run.last_seq||0}${run.last_error?' · '+run.last_error:''}`;
|
||
$('lsResults').textContent=fmtN(st.results);$('lsDownloads').textContent=fmtN(st.downloads_completed);$('lsClaims').textContent=fmtN(st.claims);$('lsNew').textContent=fmtN(st.new_evidence);$('lsDup').textContent=fmtN(st.duplicates);$('lsConfirm').textContent=fmtN(st.corroborations);$('liveQueries').innerHTML=(run.queries||[]).map(q=>`<span class="queryChip" title="${esc(q)}">${esc(q)}</span>`).join('')||'<span class="muted small">Noch keine Query geplant.</span>';
|
||
let terminals=new Set(events.filter(e=>['download.completed','source.rejected'].includes(e.type)&&e.url).map(e=>e.url));let groups={queries:[],results:[],downloads:[],claims:[],dedupe:[],rejected:[]};for(let ev of events){let lane=researchLane(ev);(groups[lane]||groups.queries).push(ev)}
|
||
let ids={queries:'laneQueries',results:'laneResults',downloads:'laneDownloads',claims:'laneClaims',dedupe:'laneDedupe',rejected:'laneRejected'},counts={queries:'lcQueries',results:'lcResults',downloads:'lcDownloads',claims:'lcClaims',dedupe:'lcDedupe',rejected:'lcRejected'};for(let k of Object.keys(ids)){let xs=groups[k].slice(-40).reverse();$(counts[k]).textContent=fmtN(groups[k].length);$(ids[k]).innerHTML=xs.length?xs.map(e=>liveEventCard(e,terminals)).join(''):'<div class="emptyLane">Noch keine Events.</div>'}
|
||
}
|
||
async function loadResearchHistory(){if(!researchWatch.goalId)return;try{let xs=await req('/api/v1/goals/'+encodeURIComponent(researchWatch.goalId)+'/research/history?limit=8');$('researchHistory').innerHTML=xs.length?xs.map(r=>`<div class="historyRun"><div class="itemTitle"><b>${new Date(r.started_at).toLocaleString()}</b><span class="pill ${r.status==='completed'?'good':r.status==='running'?'warn':r.status==='failed'?'bad':''}">${esc(r.status)}</span></div><div class="meta"><span>${r.stats?.results||0} Treffer</span><span>${r.stats?.new_evidence||0} neu</span><span>${r.stats?.corroborations||0} bestätigt</span></div></div>`).join(''):'<span class="muted small">Noch keine Research-Läufe.</span>'}catch(e){console.warn(e)}}
|
||
async function pollGoalResearch(){if(!researchWatch.goalId||currentView!=='goals')return;try{let q=`?after=${researchWatch.after}`+(researchWatch.runId?`&run_id=${encodeURIComponent(researchWatch.runId)}`:'');let d=await req('/api/v1/goals/'+encodeURIComponent(researchWatch.goalId)+'/research/live'+q);if(!d.run){researchWatch.run=null;researchWatch.events=[];researchWatch.after=0;renderResearchLive();return}if(d.reset||researchWatch.runId!==d.run.id){researchWatch.runId=d.run.id;researchWatch.after=0;researchWatch.events=[]}researchWatch.run=d.run;if(d.events?.length){researchWatch.events.push(...d.events);if(researchWatch.events.length>500)researchWatch.events=researchWatch.events.slice(-500);researchWatch.after=Math.max(researchWatch.after,...d.events.map(e=>Number(e.seq||0)))}renderResearchLive();if(d.run.status!=='running')loadResearchHistory()}catch(e){$('liveRunMeta').textContent='Live-Research konnte nicht geladen werden: '+e.message;$('livePulse').className='livePulse error'}}
|
||
function openGoalResearch(id,title){researchWatch.goalId=id;researchWatch.goalTitle=title||id;researchWatch.runId='';researchWatch.after=0;researchWatch.events=[];researchWatch.run=null;$('goalResearchLive').classList.add('show');$('liveGoalTitle').textContent='Live Research · '+researchWatch.goalTitle;renderResearchLive();loadResearchHistory();startResearchPolling();setTimeout(()=>$('goalResearchLive').scrollIntoView({behavior:'smooth',block:'start'}),20)}
|
||
$('liveCloseBtn').addEventListener('click',()=>{$('goalResearchLive').classList.remove('show');stopResearchPolling()});$('liveRefreshBtn').addEventListener('click',()=>{researchWatch.after=0;researchWatch.events=[];researchWatch.runId='';pollGoalResearch();loadResearchHistory()});
|
||
async function loadGoals(){try{let xs=await req('/api/v1/goals');xs=Array.isArray(xs)?xs:[];$('goalsList').innerHTML=xs.length?xs.map(g=>{let active=g.status==='active',paused=g.status==='paused';return `<div class="goal ${paused?'goalPaused':''}"><div class="goalHead"><div><h3>${esc(g.title)}</h3><div class="meta"><span>Priorität ${g.priority}</span><span>${g.research_enabled?'⌕ Research':'ohne Research'}</span><span>${g.auto_run?'AUTO':'MANUELL'}</span></div></div><span class="pill ${active?'good':paused?'warn':''}">${paused?'PAUSIERT':esc(g.status)}</span></div><p class="muted small">${esc(g.description||'')}</p><div class="progress"><span style="width:${Math.round((g.progress||0)*100)}%"></span></div><div class="meta"><span>${Math.round((g.progress||0)*100)}%</span><span>${esc(g.progress_reason||'Fortschritt noch nicht messbar')}</span><span>Intervall ${g.interval_minutes||'–'} min</span><span>${paused?'Scheduler angehalten':('Nächster: '+(g.next_cycle_at?when(g.next_cycle_at):'–'))}</span></div>${g.research_enabled?`<div class="meta"><span>${g.research_evidence||0} Evidenzen</span><span>${g.research_sources||0} Quellen</span><span>${g.research_corroborations||0} bestätigt</span>${g.last_staging_draft_id?`<span class="pill good">Staging ${esc(g.last_staging_draft_id)}</span>`:''}</div>`:''}${g.next_action?`<p><b>Nächste Aktion:</b> ${esc(g.next_action)}</p>`:''}${g.last_staging_error?`<p class="error small"><b>Staging:</b> ${esc(g.last_staging_error)}</p>`:''}${g.last_error?`<p class="error small">${esc(g.last_error)}</p>`:''}<div class="goalActions">${active?`<button class="btn secondary" data-goal-action="cycle" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Jetzt lernen</button><button class="btn ghost" data-goal-action="pause" data-goal-id="${esc(g.id)}">Pausieren</button>`:''}${paused?`<button class="btn secondary" data-goal-action="resume" data-goal-id="${esc(g.id)}">Fortsetzen</button>`:''}${g.research_enabled?`<button class="btn ghost" data-research-live="${esc(g.id)}" data-goal-title="${esc(g.title)}">Live Research</button>`:''}<button class="btn ghost" data-focus="${esc(g.id)}">Graph</button><button class="btn danger" data-goal-action="delete" data-goal-id="${esc(g.id)}" data-goal-title="${esc(g.title)}">Löschen</button></div></div>`}).join(''):'<p class="muted">Noch keine Ziele.</p>';$$('[data-goal-action]').forEach(b=>b.addEventListener('click',()=>goalAction(b)));$$('[data-research-live]').forEach(b=>b.addEventListener('click',()=>openGoalResearch(b.dataset.researchLive,b.dataset.goalTitle)));$$('[data-focus]').forEach(b=>b.addEventListener('click',()=>{switchView('knowledge');$('knowledgeQuery').value='goal:'+b.dataset.focus;searchKnowledge()}))}catch(e){toast(e.message,true)}}
|
||
async function goalAction(b){let id=b.dataset.goalId,action=b.dataset.goalAction;if(action==='delete'&&!confirm(`Ziel „${b.dataset.goalTitle||id}“ wirklich löschen? Bereits gelerntes Wissen bleibt erhalten.`))return;if(action==='cycle')openGoalResearch(id,b.dataset.goalTitle||id);let old=b.textContent;try{b.disabled=true;b.textContent=action==='cycle'?'läuft …':action==='delete'?'lösche …':'speichere …';let path='/api/v1/goals/'+encodeURIComponent(id)+(action==='cycle'?'/cycle':action==='pause'?'/pause':action==='resume'?'/resume':'');let d=await req(path,{method:action==='delete'?'DELETE':'POST',body:action==='delete'?undefined:'{}'});if(action==='cycle')toast(`Zyklus fertig · Evaluation ${Number(d.evaluation).toFixed(2)} · ${d.sources_ingested||0} Quellen`);else if(action==='pause')toast('Ziel pausiert. Der Scheduler verarbeitet es nicht weiter.');else if(action==='resume')toast('Ziel wieder aktiv. Nächster Lauf wurde eingeplant.');else{toast('Ziel gelöscht. Bereits gelerntes Wissen bleibt erhalten.');if(researchWatch.goalId===id){$('goalResearchLive').classList.remove('show');stopResearchPolling();researchWatch.goalId=''}}await loadGoals();refreshAll()}catch(e){toast(e.message,true)}finally{b.disabled=false;b.textContent=old}}
|
||
$('goalCreateBtn').addEventListener('click',async()=>{let title=$('goalTitle').value.trim();if(!title)return toast('Titel fehlt.',true);try{let g=await req('/api/v1/goals',{method:'POST',body:JSON.stringify({title,description:$('goalDesc').value,target:$('goalTarget').value,priority:Number($('goalPriority').value||50),interval_minutes:Number($('goalInterval').value||10),status:'active'})});toast(`Ziel ${g.title} angelegt – erster Lauf ist eingeplant.`);$('goalTitle').value='';$('goalDesc').value='';loadGoals();refreshAll()}catch(e){toast(e.message,true)}});$('runAutonomyBtn').addEventListener('click',async()=>{try{let d=await req('/admin/api/autonomy',{method:'POST',body:'{}'});toast(`${d.cycles?.length||0} fällige Zyklen ausgeführt.`);loadGoals()}catch(e){toast(e.message,true)}});
|
||
|
||
// ---- Chat ----
|
||
$('chatBtn').addEventListener('click',async()=>{let input=document.getElementById('chatPrompt').value.trim();if(!input)return toast('Nachricht fehlt.',true);chatAbort=new AbortController();$('chatAbortBtn').disabled=false;$('chatBtn').disabled=true;let start=Date.now(),timer=setInterval(()=>$('chatElapsed').textContent=Math.round((Date.now()-start)/1000)+' s',1000);$('chatOutput').textContent='Ollama / NeuroForge arbeitet …';try{let r=await fetch('/api/v1/chat',{method:'POST',headers:tokenHeaders(),signal:chatAbort.signal,body:JSON.stringify({session_id:$('chatSession').value,input,provider:$('chatProvider').value})}),txt=await r.text(),d;try{d=JSON.parse(txt)}catch{d=txt}if(!r.ok)throw new Error(d.error||d);$('chatOutput').textContent=(d.answer||'')+'\n\n— '+(d.provider||'')+' · '+(d.model||'')+' · Recall '+(d.recalled?.length||0)+' · Reward '+Number(d.auto_reward||0).toFixed(3);refreshAll()}catch(e){$('chatOutput').textContent=e.name==='AbortError'?'Abgebrochen.':e.message}finally{clearInterval(timer);$('chatElapsed').textContent='';$('chatAbortBtn').disabled=true;$('chatBtn').disabled=false;chatAbort=null}});$('chatAbortBtn').addEventListener('click',()=>chatAbort?.abort());
|
||
|
||
// ---- Models ----
|
||
async function loadModels(){try{let d=await req('/admin/api/model-routing');$('modelJSON').value=JSON.stringify(d,null,2);let nodes=d.ollama||[];$('modelCards').innerHTML=nodes.map(n=>`<div class="item"><div class="itemTitle"><b>${esc(n.name||n.id)}</b><span class="pill ${n.enabled?'good':''}">${n.enabled?'aktiv':'aus'}</span></div><p class="mono small">${esc(n.base_url)}</p><div class="meta"><span>Chat: ${esc(n.chat_model)}</span><span>Embed: ${esc(n.embedding_model)}</span><span>ctx ${n.num_ctx||'default'}</span><span>think ${esc(n.think||'off')}</span></div></div>`).join('')}catch(e){toast(e.message,true)}}$('modelSaveBtn').addEventListener('click',async()=>{try{let d=JSON.parse($('modelJSON').value);await req('/admin/api/model-routing',{method:'PUT',body:JSON.stringify(d)});toast('Modelle & Routing gespeichert.');loadModels()}catch(e){toast(e.message,true)}});$('modelHealthBtn').addEventListener('click',async()=>{try{$('modelHealth').textContent='prüfe …';let d=await req('/admin/api/provider-health',{method:'POST',body:'{}'});$('modelHealth').textContent=JSON.stringify(d,null,2)}catch(e){$('modelHealth').textContent=e.message}});
|
||
|
||
// ---- Operations ----
|
||
async function loadOps(){try{let [raw,st,cfg]=await Promise.all([req('/admin/api/status'),req('/admin/api/storage'),req('/admin/api/config')]),s=normalizeStatus(raw);$('opsIndex').innerHTML=`<div class="item">HNSW <b>${fmtN(s.hnsw_nodes)}</b></div><div class="item">Disk-PQ <b>${fmtN(s.disk_pq_items)}</b></div><div class="item">Mode <b>${esc(s.index_mode||'–')}</b></div>`;$('opsStorage').innerHTML=`<div class="item">Revision <b>${fmtN(s.revision)}</b></div><div class="item">Segmente <b>${fmtN(st.segments?.files||st.memory_segments?.files||0)}</b></div><div class="item">Disk <b>${fmtBytes(st.segments?.bytes||st.memory_segments?.bytes||0)}</b></div>`;$('opsObs').innerHTML=`<div class="item">Pending jobs <b>${fmtN(s.pending_jobs)}</b></div><div class="item">Remote shards <b>${fmtN(s.remote_shards)}</b></div><div class="item">Letzter Fehler <span class="small">${esc(s.maintenance?.last_error||'keiner')}</span></div>`;$('configJSON').value=JSON.stringify(cfg,null,2)}catch(e){toast(e.message,true)}}$('configSaveBtn').addEventListener('click',async()=>{try{let c=JSON.parse($('configJSON').value);await req('/admin/api/config',{method:'PUT',body:JSON.stringify(c)});toast('Konfiguration gespeichert.')}catch(e){toast(e.message,true)}});$('checkpointBtn').addEventListener('click',()=>opPost('/admin/api/checkpoint','Checkpoint erstellt'));$('compactBtn').addEventListener('click',()=>opPost('/admin/api/storage/compact','Segmente kompaktiert'));$('pqBuildBtn').addEventListener('click',()=>opPost('/admin/api/index/disk/rebuild','PQ-Index neu gebaut'));$('indexMergeBtn').addEventListener('click',()=>opPost('/admin/api/index/merge','Index gemerged'));async function opPost(path,msg){try{await req(path,{method:'POST',body:'{}'});toast(msg);loadOps()}catch(e){toast(e.message,true)}}
|
||
|
||
async function boot(){if(!admin){await setToken();if(!admin)return}await refreshAll();setInterval(()=>{if(admin&&['dashboard','goals'].includes(currentView))refreshAll()},15000)}boot();
|
||
</script>
|
||
</body></html> |