update
This commit is contained in:
@@ -5,7 +5,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { api } from '../api.js'
|
||||
|
||||
const props = defineProps({ state: Object, topic: String })
|
||||
const emit = defineEmits(['reload'])
|
||||
const emit = defineEmits(['reload', 'fehler'])
|
||||
|
||||
// Agenten-Zeit lokal weiterticken: der Server liefert laufzeit nur mit dem
|
||||
// State-Snapshot — zwischen Deltas (lange LLM-Calls) fröre die Anzeige ein.
|
||||
@@ -41,6 +41,7 @@ const EBENEN = [
|
||||
{ key: 'inventar', titel: 'Atome' },
|
||||
{ key: 'artefakte', titel: 'Artefakte' },
|
||||
{ key: 'struktur', titel: 'Bausteine' },
|
||||
{ key: 'diagramme', titel: 'Diagramme' },
|
||||
{ key: 'guide', titel: 'Guide' },
|
||||
]
|
||||
|
||||
@@ -79,6 +80,10 @@ function karten(key) {
|
||||
return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`,
|
||||
badges: [[b.status, statusFarbe(b.status)]] }))
|
||||
}
|
||||
if (key === 'diagramme') {
|
||||
return (s.diagramme || []).map((d) => ({ id: d.id, text: `${d.ord + 1}. ${d.titel}`,
|
||||
badges: [[d.status, statusFarbe(d.status)], [`${d.typ} · ${d.quelle}`, 'blau']] }))
|
||||
}
|
||||
return s.bausteine.map((b) => ({ id: b.id, text: `${b.ord + 1}. ${b.titel}`,
|
||||
badges: [[b.stage || 'offen', b.stage === 'done' ? 'gruen' : 'blau']] }))
|
||||
}
|
||||
@@ -100,21 +105,21 @@ async function menueGenerieren() {
|
||||
const m = menue.value
|
||||
menue.value = null
|
||||
if (!m || !generierenErlaubt(m.idx)) return
|
||||
await api.start(props.topic)
|
||||
emit('reload')
|
||||
try { await api.start(props.topic); emit('reload') }
|
||||
catch (e) { emit('fehler', String(e)) } // sonst schlägt der Start still fehl
|
||||
}
|
||||
|
||||
async function menueEntfernen() {
|
||||
const m = menue.value
|
||||
menue.value = null
|
||||
if (!m) return
|
||||
await api.ebeneEntfernen(props.topic, m.key)
|
||||
emit('reload')
|
||||
try { await api.ebeneEntfernen(props.topic, m.key); emit('reload') }
|
||||
catch (e) { emit('fehler', String(e)) }
|
||||
}
|
||||
|
||||
async function autoSetzen(key, ev) {
|
||||
await api.setAuto(props.topic, key, ev.target.checked)
|
||||
emit('reload')
|
||||
try { await api.setAuto(props.topic, key, ev.target.checked); emit('reload') }
|
||||
catch (e) { emit('fehler', String(e)) }
|
||||
}
|
||||
|
||||
const dauer = (s) => (s >= 3600 ? `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import mermaid from 'mermaid'
|
||||
import { api } from '../api.js'
|
||||
import { render, lesestat } from '../markdown.js'
|
||||
|
||||
// securityLevel:'strict' → kein foreignObject/HTML-Label (umgeht DOMPurify-Filter),
|
||||
// keine Klick-Handler. startOnLoad:false → wir rendern selbst nach jedem Einblenden.
|
||||
mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'neutral' })
|
||||
let mmId = 0
|
||||
|
||||
const props = defineProps({ topic: String, state: Object })
|
||||
const daten = ref(null)
|
||||
const level = ref('E') // Durchgang-Wahl: jede Stufe ist ein eigener Lese-Durchgang
|
||||
@@ -15,10 +21,14 @@ watch(fokus, (f) => document.body.classList.toggle('vollbild', f))
|
||||
onUnmounted(() => document.body.classList.remove('vollbild'))
|
||||
const aktiv = ref(0)
|
||||
const kapEls = []
|
||||
const scrollEl = ref(null)
|
||||
|
||||
let ladeSeq = 0
|
||||
async function laden() {
|
||||
if (!props.topic) return
|
||||
daten.value = await api.guide(props.topic)
|
||||
if (!props.topic) { daten.value = null; return }
|
||||
const seq = ++ladeSeq
|
||||
const d = await api.guide(props.topic)
|
||||
if (seq === ladeSeq) daten.value = d // späte Antwort eines alten Topics verwerfen
|
||||
}
|
||||
watch(() => props.topic, laden, { immediate: true })
|
||||
watch(() => props.state?.run?.status, laden) // nach Lauf-Ende neu ziehen
|
||||
@@ -36,6 +46,36 @@ function html(s) {
|
||||
return htmlCache.get(key)
|
||||
}
|
||||
|
||||
// „Prüfe dich"-Antworten ebenfalls cachen — sonst rechnet jeder Scroll-Tick
|
||||
// Markdown+KaTeX für alle sichtbaren Antworten neu (genau der Kostenpfad oben).
|
||||
function htmlAntwort(s, fi) {
|
||||
const key = `a|${s.baustein}|${fi}`
|
||||
if (!htmlCache.has(key)) htmlCache.set(key, render(s.fragen[fi].antwort))
|
||||
return htmlCache.get(key)
|
||||
}
|
||||
|
||||
// Mermaid rendert asynchron ins DOM — nach jedem Einblenden (Scroll/Level/Ansicht)
|
||||
// die noch nicht verarbeiteten ```mermaid-Blöcke im sichtbaren Bereich rendern.
|
||||
async function renderMermaid() {
|
||||
await nextTick()
|
||||
const wurzel = scrollEl.value
|
||||
if (!wurzel) return
|
||||
for (const code of wurzel.querySelectorAll('code.language-mermaid:not([data-mm])')) {
|
||||
code.setAttribute('data-mm', '1')
|
||||
const pre = code.closest('pre')
|
||||
try {
|
||||
const { svg } = await mermaid.render(`mm-${mmId++}`, code.textContent)
|
||||
const figur = document.createElement('div')
|
||||
figur.className = 'mermaid-figur'
|
||||
figur.innerHTML = svg
|
||||
pre?.replaceWith(figur)
|
||||
} catch {
|
||||
pre?.classList.add('mermaid-fehler') // kaputt: als Codeblock stehen lassen
|
||||
}
|
||||
}
|
||||
}
|
||||
watch([aktiv, ansicht, level, daten], renderMermaid, { flush: 'post' })
|
||||
|
||||
// Nur aktives Kapitel ±1 im DOM; der Rest ist ein Platzhalter mit Schätzhöhe
|
||||
const sichtbar = (i) => Math.abs(i - aktiv.value) <= 1
|
||||
|
||||
@@ -49,11 +89,13 @@ function setAnsicht(a) {
|
||||
ansicht.value = a
|
||||
}
|
||||
|
||||
// Nur Durchgänge anbieten, die es in diesem Thema gibt (ohne E- oder
|
||||
// S-Bausteine überspringt der Toggle die Stufe)
|
||||
// Nur Durchgänge anbieten, die WIRKLICH Inhalt haben (≥1 gefüllte Section) —
|
||||
// sonst schaltet der Toggle in einen leeren Durchgang und läuft in eine Sackgasse
|
||||
const levels = computed(() => {
|
||||
const da = new Set((daten.value?.kapitel || []).map((k) => k.level))
|
||||
const folge = ['E', 'M', 'S'].filter((l) => da.has(l))
|
||||
const kaps = daten.value?.kapitel || []
|
||||
const hatInhalt = (l) => kaps.some((k) => k.level === l
|
||||
&& k.sections.some((s) => ((s.lang || '') + (s.kompakt || '')).trim()))
|
||||
const folge = ['E', 'M', 'S'].filter(hatInhalt)
|
||||
return folge.length ? folge : ['E']
|
||||
})
|
||||
watch(levels, (l) => { if (!l.includes(level.value)) level.value = l[0] })
|
||||
@@ -74,7 +116,13 @@ const anzeige = computed(() => {
|
||||
}
|
||||
return out
|
||||
})
|
||||
watch([level, ansicht, daten], () => { kapEls.length = 0; aktiv.value = 0 })
|
||||
watch([level, ansicht, daten], () => {
|
||||
kapEls.length = 0
|
||||
aktiv.value = 0
|
||||
// Scrollposition zurücksetzen — sonst steht der Viewport nach dem Wechsel
|
||||
// mitten in Platzhalter-Divs (echter Inhalt nur oben) und wirkt leer.
|
||||
nextTick(() => { if (scrollEl.value) scrollEl.value.scrollTop = 0 })
|
||||
})
|
||||
|
||||
// Lesezeit des SICHTBAREN Umfangs: 150 Wörter/min (technischer Text)
|
||||
// + 5 s je Display-Formel (Medium ~265 wpm gilt nicht für Mathe)
|
||||
@@ -121,7 +169,7 @@ function onScroll(e) {
|
||||
|
||||
<template>
|
||||
<div class="guide-layout" :class="{ fokus }">
|
||||
<nav v-if="anzeige.length" class="guide-nav">
|
||||
<nav v-if="daten" class="guide-nav">
|
||||
<div class="schalter-zeile">
|
||||
<div class="schalter">
|
||||
<button :class="{ aktiv: ansicht === 'erklaerend' }" title="ausführlich (viel Text)"
|
||||
@@ -154,7 +202,7 @@ function onScroll(e) {
|
||||
<span class="zeit">{{ zeit(stats.je[i]?.minuten || 0) }}</span>
|
||||
</a>
|
||||
</nav>
|
||||
<div class="seite guide-text" @scroll="onScroll">
|
||||
<div ref="scrollEl" class="seite guide-text" @scroll="onScroll">
|
||||
<button v-if="fokus" class="fokus-aus" title="Fokus beenden"
|
||||
@click="fokus = false">✕</button>
|
||||
<template v-if="daten">
|
||||
@@ -175,7 +223,7 @@ function onScroll(e) {
|
||||
<summary>Antworten zu „Prüfe dich"</summary>
|
||||
<div v-for="(f, fi) in s.fragen" :key="fi" class="antwort">
|
||||
<div class="frage">{{ f.frage }}</div>
|
||||
<div v-html="render(f.antwort)"></div>
|
||||
<div v-html="htmlAntwort(s, fi)"></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ const props = defineProps({ run: Object })
|
||||
const daten = ref(null)
|
||||
|
||||
async function laden() {
|
||||
if (!props.run) return
|
||||
if (!props.run) { daten.value = null; return } // sonst bleiben Alt-Daten stehen
|
||||
daten.value = await api.kennzahlen(props.run.id)
|
||||
}
|
||||
watch(() => props.run?.id, laden, { immediate: true })
|
||||
|
||||
@@ -7,6 +7,7 @@ const props = defineProps({ topic: String })
|
||||
const stapel = ref([])
|
||||
const zeigeAntwort = ref(false)
|
||||
const fertigGelernt = ref(0)
|
||||
const sende = ref(false) // Doppelklick-Sperre: sonst POSTet die Karte zweimal
|
||||
|
||||
async function laden() {
|
||||
if (!props.topic) return
|
||||
@@ -17,11 +18,17 @@ async function laden() {
|
||||
watch(() => props.topic, laden, { immediate: true })
|
||||
|
||||
async function antworten(richtig) {
|
||||
const karte = stapel.value[0]
|
||||
await api.antwort(karte.id, richtig)
|
||||
stapel.value = richtig ? stapel.value.slice(1) : [...stapel.value.slice(1), karte]
|
||||
if (richtig) fertigGelernt.value++
|
||||
zeigeAntwort.value = false
|
||||
if (sende.value) return
|
||||
sende.value = true
|
||||
try {
|
||||
const karte = stapel.value[0]
|
||||
await api.antwort(karte.id, richtig)
|
||||
stapel.value = richtig ? stapel.value.slice(1) : [...stapel.value.slice(1), karte]
|
||||
if (richtig) fertigGelernt.value++
|
||||
zeigeAntwort.value = false
|
||||
} finally {
|
||||
sende.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -36,8 +43,10 @@ async function antworten(richtig) {
|
||||
<template v-else>
|
||||
<div class="antwort markdown" v-html="render(stapel[0].inhalt.antwort)"></div>
|
||||
<div style="display: flex; gap: 10px; justify-content: center">
|
||||
<button style="background: var(--gruen)" @click="antworten(true)">Richtig</button>
|
||||
<button style="background: var(--rot)" @click="antworten(false)">Falsch</button>
|
||||
<button style="background: var(--gruen)" :disabled="sende"
|
||||
@click="antworten(true)">Richtig</button>
|
||||
<button style="background: var(--rot)" :disabled="sende"
|
||||
@click="antworten(false)">Falsch</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user