This commit is contained in:
team3
2026-07-23 16:07:36 +02:00
commit cb68cd671b
88 changed files with 10029 additions and 0 deletions

182
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,182 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from "vue";
import { api, wsVerbinden } from "./api.js";
import { layout, store } from "./store.js";
import Graph from "./components/Graph.vue";
import Guide from "./components/Guide.vue";
import Kennzahlen from "./components/Kennzahlen.vue";
const tab = ref("graph");
const navOffen = ref(false);
const fehler = ref("");
const neuName = ref("");
let wsTrennen = null;
let debounceId = null;
let pollId = null;
const laeuft = computed(() => store.run?.status === "running");
const quellenMax = computed(() => {
const t = store.topics.find((x) => x.name === store.topic);
return t?.quellen_max ?? 24;
});
async function quellenSetzen(ev) {
// Regler-Position 1 = ∞ (API-Wert 0); echte Deckel beginnen bei 2,
// denn 1 Quelle kann nie bestätigen (Konsens braucht 2 unabhängige)
const pos = parseInt(ev.target.value, 10);
const wert = pos === 1 ? 0 : pos;
try {
await api.einstellen(store.topic, wert);
await topicsLaden();
} catch (e) {
fehler.value = String(e);
}
}
async function topicsLaden() {
store.topics = await api.topics();
if (!store.topic && store.topics.length) store.topic = store.topics[0].name;
}
async function graphLaden() {
if (!store.topic) return;
try {
const g = await api.graph(store.topic);
layout.value = g.layout;
store.zaehler = g.zaehler;
store.gates = g.gates;
store.run = g.run;
} catch (e) {
fehler.value = String(e);
}
}
function onWs(ev) {
if (ev.typ === "verbunden") return alles();
clearTimeout(debounceId);
debounceId = setTimeout(() => {
graphLaden();
topicsLaden();
}, 250);
}
async function alles() {
await topicsLaden();
await graphLaden();
}
async function waehlen(name) {
store.topic = name;
navOffen.value = false;
await graphLaden();
}
async function anlegen() {
if (!neuName.value.trim()) return;
try {
await api.anlegen(neuName.value.trim(), "");
neuName.value = "";
await alles();
} catch (e) {
fehler.value = String(e);
}
}
async function aktion(fn) {
fehler.value = "";
try {
await fn(store.topic);
await graphLaden();
} catch (e) {
fehler.value = String(e);
}
}
async function starten() {
let budget = null;
const r = store.run;
const knapp = r && r.budget_tokens > 0 &&
(r.verbraucht || 0) >= r.budget_tokens * 0.9;
if (r?.status === "budget" || (r?.status === "paused" && knapp)) {
const alt = r.budget_tokens || 2000000;
const eingabe = window.prompt(
`Budget ${r.status === "budget" ? "erschöpft" : "fast erschöpft"} ` +
`(${((r.verbraucht || 0) / 1e6).toFixed(1)}M verbraucht). ` +
"Neues Token-Budget für diesen Lauf:",
String(alt + 2000000),
);
if (eingabe === null) return;
budget = parseInt(eingabe, 10) || null;
}
await aktion((t) => api.start(t, budget));
}
async function reset() {
if (!window.confirm(`${store.topic}" komplett zurücksetzen? ` +
"Quellen, Atome und Guide werden gelöscht."))
return;
await aktion(api.reset);
await topicsLaden();
}
onMounted(() => {
alles();
wsTrennen = wsVerbinden(onWs);
pollId = setInterval(() => laeuft.value && graphLaden(), 5000);
});
onUnmounted(() => {
wsTrennen?.();
clearInterval(pollId);
});
</script>
<template>
<div class="app">
<header class="topbar">
<button class="burger" @click="navOffen = !navOffen"></button>
<nav class="tabs">
<button :class="{ aktiv: tab === 'graph' }" @click="tab = 'graph'">Pipeline</button>
<button :class="{ aktiv: tab === 'guide' }" @click="tab = 'guide'">Guide</button>
<button :class="{ aktiv: tab === 'kennzahlen' }" @click="tab = 'kennzahlen'">
Kennzahlen
</button>
</nav>
<span class="spacer" />
<span v-if="store.run" class="runstatus" :data-status="store.run.status">
{{ store.run.status }}<template v-if="store.run.grund"> · {{ store.run.grund }}</template>
</span>
<button v-if="!laeuft" class="primaer" :disabled="!store.topic"
@click="starten()">Start</button>
<button v-else @click="aktion(api.pause)">Pause</button>
<button v-if="!laeuft && store.topic" title="Pipeline auf null zurücksetzen"
@click="reset()">Reset</button>
<label v-if="store.topic" class="quellen-regler"
:title="'Quellen-Deckel — ganz links: unbegrenzt, es stoppt nur die Sättigung'">
<span>Quellen: <b>{{ quellenMax === 0 ? "∞" : quellenMax }}</b></span>
<input type="range" min="1" max="48" step="1"
:value="quellenMax === 0 ? 1 : quellenMax"
:disabled="laeuft" @change="quellenSetzen($event)" />
</label>
</header>
<aside class="sidebar" :class="{ offen: navOffen }">
<form @submit.prevent="anlegen">
<input v-model="neuName" placeholder="Neues Thema…" />
</form>
<ul>
<li v-for="t in store.topics" :key="t.name"
:class="{ aktiv: t.name === store.topic }" @click="waehlen(t.name)">
<span>{{ t.titel || t.name }}</span>
<small>{{ t.status }}</small>
</li>
</ul>
</aside>
<main>
<p v-if="fehler" class="fehler" @click="fehler = ''">{{ fehler }}</p>
<Graph v-if="tab === 'graph'" />
<Guide v-else-if="tab === 'guide'" />
<Kennzahlen v-else />
</main>
</div>
</template>