init
This commit is contained in:
182
frontend/src/App.vue
Normal file
182
frontend/src/App.vue
Normal 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>
|
||||
Reference in New Issue
Block a user