This commit is contained in:
team3
2026-07-10 17:14:17 +02:00
parent 58bb600ba3
commit 917b3ac702
6 changed files with 83 additions and 13 deletions

View File

@@ -92,6 +92,20 @@ async function stoppen() {
await stateLaden()
}
const transferOk = ref(false) // nur im lokalen Dev-Betrieb (Server: 403)
const uebertragung = ref('')
async function uebertragen(richtung) {
uebertragung.value = richtung === 'push' ? 'pusht' : 'pullt'
try {
fehler.value = ''
await api.transfer(topic.value, richtung)
await topicsLaden()
await stateLaden()
} catch (e) { fehler.value = String(e) }
finally { uebertragung.value = '' }
}
async function waehlen(name) {
topic.value = name
await stateLaden()
@@ -101,6 +115,7 @@ let poll = null // Fallback, falls während langer LLM-Calls keine WS-Deltas kom
onMounted(async () => {
themeAnwenden()
api.transferInfo().then((i) => { transferOk.value = i.verfuegbar }).catch(() => {})
await topicsLaden()
await stateLaden()
wsSchliessen = wsVerbinden(onWs)
@@ -163,6 +178,15 @@ function runBadge(run) {
</div>
<button @click="starten" :disabled="!topic || state?.run?.status === 'running'">Start</button>
<button class="sekundaer" @click="stoppen" :disabled="state?.run?.status !== 'running'">Stop</button>
<template v-if="transferOk">
<button class="sekundaer" title="Thema auf den Server pushen (überschreibt dort)"
:disabled="!topic || !!uebertragung || state?.run?.status === 'running'"
@click="uebertragen('push')"> Push</button>
<button class="sekundaer" title="Thema vom Server pullen (überschreibt lokal)"
:disabled="!topic || !!uebertragung || state?.run?.status === 'running'"
@click="uebertragen('pull')"> Pull</button>
<span v-if="uebertragung" class="badge blau">{{ uebertragung }}</span>
</template>
<span v-if="state?.run" class="badge" :class="runBadge(state.run).farbe">{{ runBadge(state.run).text }}</span>
<span v-if="state" class="badge">{{ tok(state.verbraucht) }} Tokens</span>
<span v-if="state?.run?.grund" class="badge gelb">{{ state.run.grund }}</span>

View File

@@ -28,6 +28,9 @@ export const api = {
antwort: (artefakt_id, richtig) =>
req('/api/ueben/antwort', { method: 'POST', body: JSON.stringify({ artefakt_id, richtig }) }),
kennzahlen: (runId) => req(`/api/runs/${runId}/kennzahlen`),
transferInfo: () => req('/api/transfer'),
transfer: (topic, richtung) =>
req(`/api/topics/${topic}/transfer/${richtung}`, { method: 'POST', body: '{}' }),
}
export function wsVerbinden(onEvent) {

View File

@@ -41,7 +41,7 @@ const sichtbar = (i) => Math.abs(i - aktiv.value) <= 1
function hoehe(i) {
const st = stats.value.je[i]
const sections = daten.value?.kapitel?.[i]?.sections?.length || 1
const sections = anzeige.value[i]?.sections?.length || 1
return Math.round((st?.woerter || 200) / 11 * 28 + sections * 180)
}
@@ -56,10 +56,24 @@ function levelToggle() {
laden()
}
// Nur Kapitel/Bausteine mit Inhalt im gewählten Umfang anzeigen — der
// Level-Filter ließe sonst leere Titel-Hüllen stehen (36/66 Bausteine ohne E)
const anzeige = computed(() => {
const out = []
for (const kap of daten.value?.kapitel || []) {
const sections = kap.sections.filter((s) => ansicht.value === 'kompakt'
? (s.kompakt || s.lang || '').trim()
: lesestat(gefiltert(s.lang, level.value)).woerter >= 10)
if (sections.length) out.push({ ...kap, sections })
}
return out
})
watch([level, ansicht, daten], () => { kapEls.length = 0; aktiv.value = 0 })
// Lesezeit des SICHTBAREN Umfangs: 150 Wörter/min (technischer Text)
// + 5 s je Display-Formel (Medium ~265 wpm gilt nicht für Mathe)
const stats = computed(() => {
const je = (daten.value?.kapitel || []).map((kap) => {
const je = anzeige.value.map((kap) => {
let woerter = ansicht.value === 'kompakt' ? 0 : lesestat(kap.intro).woerter
let formeln = 0
for (const s of kap.sections) {
@@ -102,7 +116,7 @@ function onScroll(e) {
<template>
<div class="guide-layout" :class="{ fokus }">
<nav v-if="daten?.kapitel?.length" class="guide-nav">
<nav v-if="anzeige.length" class="guide-nav">
<div class="schalter-zeile">
<div class="schalter">
<button :class="{ aktiv: ansicht === 'erklaerend' }" title="ausführlich (viel Text)"
@@ -126,10 +140,10 @@ function onScroll(e) {
@click="fokus = true"></button>
</div>
<div class="fortschritt">
<div>Kapitel {{ aktiv + 1 }} / {{ daten.kapitel.length }}</div>
<div>Kapitel {{ aktiv + 1 }} / {{ anzeige.length }}</div>
<div>{{ stats.woerter.toLocaleString('de-DE') }} Wörter · {{ zeit(stats.minuten) }}</div>
</div>
<a v-for="(kap, i) in daten.kapitel" :key="i" :class="{ aktiv: i === aktiv }"
<a v-for="(kap, i) in anzeige" :key="kap.kapitel_id" :class="{ aktiv: i === aktiv }"
@click="springe(i)">
<span>{{ i + 1 }}. {{ kap.titel }}</span>
<span class="zeit">{{ zeit(stats.je[i]?.minuten || 0) }}</span>
@@ -139,7 +153,7 @@ function onScroll(e) {
<button v-if="fokus" class="fokus-aus" title="Fokus beenden"
@click="fokus = false"></button>
<template v-if="daten">
<div v-for="(kap, i) in daten.kapitel" :key="i" class="kapitel"
<div v-for="(kap, i) in anzeige" :key="kap.kapitel_id" class="kapitel"
:ref="(el) => setKapEl(i, el)">
<h2 class="kapitel-titel">{{ i + 1 }}. {{ kap.titel }}</h2>
<template v-if="sichtbar(i)">
@@ -152,7 +166,7 @@ function onScroll(e) {
</template>
<div v-else class="kapitel-platzhalter" :style="{ height: hoehe(i) + 'px' }"></div>
</div>
<div v-if="!daten.kapitel.length" class="guide-section">Noch kein Guide generiert.</div>
<div v-if="!anzeige.length" class="guide-section">Noch kein Guide generiert.</div>
</template>
</div>
</div>