This commit is contained in:
team3
2026-06-27 09:50:50 +02:00
parent f018ca5048
commit 8a43839f18
9 changed files with 63 additions and 1189 deletions

View File

@@ -69,9 +69,9 @@ BAUSTEINE_STEPS = (
"Relevanz finden", "Relevanz wählen", "Relevanz klären", "Relevanz finden", "Relevanz wählen", "Relevanz klären",
"Gliederung", "Gliederung",
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen", "Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
"Karteikarten", "Beispiele", "Diagramme", "Karteikarten", "Beispiele",
) )
ARTEFAKT_TYPEN = ("karteikarte", "beispiel", "diagramm") ARTEFAKT_TYPEN = ("karteikarte", "beispiel")
def lade_quelle(topic: str) -> dict: def lade_quelle(topic: str) -> dict:
@@ -206,7 +206,7 @@ def _bausteine_steps(topic: str) -> tuple:
"Relevanz finden", "Relevanz wählen", "Relevanz klären", "Relevanz finden", "Relevanz wählen", "Relevanz klären",
"Gliederung", "Gliederung",
"Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen", "Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen",
"Karteikarten", "Beispiele", "Diagramme", "Karteikarten", "Beispiele",
) )
mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest mitte = base + (("Ergänzung",) if q["type"] == "projekt" else ()) + rest
return (("Quelle aufbereiten",) if q["type"] == "link" else ()) + mitte return (("Quelle aufbereiten",) if q["type"] == "link" else ()) + mitte
@@ -235,7 +235,7 @@ PHASEN = (
("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")), ("Relevanz", ("Relevanz finden", "Relevanz wählen", "Relevanz klären")),
("Gliederung", ("Gliederung",)), ("Gliederung", ("Gliederung",)),
("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")), ("Fragen", ("Fragen finden", "Fragen wählen", "Fragen klären", "Fragen prüfen")),
("Artefakte", ("Karteikarten", "Beispiele", "Diagramme")), ("Artefakte", ("Karteikarten", "Beispiele")),
) )
@@ -330,7 +330,7 @@ def _resume_step(topic: str) -> int:
# Gliederung fertig; Frage-Muster offen? # Gliederung fertig; Frage-Muster offen?
if not _frage_muster_komplett(topic): if not _frage_muster_komplett(topic):
return _step_idx(topic, "Fragen finden") return _step_idx(topic, "Fragen finden")
# Fragen fertig; Lern-Artefakte (Karteikarten/Beispiele/Diagramme) offen? # Fragen fertig; Lern-Artefakte (Karteikarten/Beispiele) offen?
if not _artefakte_komplett(files): if not _artefakte_komplett(files):
return _step_idx(topic, "Karteikarten") return _step_idx(topic, "Karteikarten")
return len(_bausteine_steps(topic)) return len(_bausteine_steps(topic))
@@ -402,7 +402,7 @@ def _reset_ab_phase(topic: str, label: str) -> None:
p.unlink(missing_ok=True) p.unlink(missing_ok=True)
# Phasen-Index: Quelle=0 · Inventar=1 · Subbausteine=2 · Fakten=3 · Stufen=4 · Relevanz=5 · Gliederung=6 · Fragen=7 · Artefakte=8 # Phasen-Index: Quelle=0 · Inventar=1 · Subbausteine=2 · Fakten=3 · Stufen=4 · Relevanz=5 · Gliederung=6 · Fragen=7 · Artefakte=8
if idx <= 8: # Artefakte (Karteikarten/Beispiele/Diagramme) if idx <= 8: # Artefakte (Karteikarten/Beispiele)
files["artefakte"].unlink(missing_ok=True) files["artefakte"].unlink(missing_ok=True)
glob_del("artefakt-*") glob_del("artefakt-*")
if idx <= 7: # Fragen if idx <= 7: # Fragen
@@ -1742,7 +1742,7 @@ async def _gliederung_block(ctx: GenContext, set_p, files: dict, entries: dict,
return plan return plan
# --- Lern-Artefakte (Karteikarten/Beispiele/Diagramme aus den Fakten) --- # --- Lern-Artefakte (Karteikarten/Beispiele aus den Fakten) ---
def _karten_schema(data): def _karten_schema(data):
"""{"karten":[{baustein,subbaustein,frage,antwort}]} → Liste (auch leer) · None bei kaputt.""" """{"karten":[{baustein,subbaustein,frage,antwort}]} → Liste (auch leer) · None bei kaputt."""
@@ -1772,20 +1772,9 @@ def _beispiel_schema(data):
return out return out
def _diagramm_schema(data): _ARTEFAKT_SCHEMA = {"karteikarte": _karten_schema, "beispiel": _beispiel_schema}
"""{"diagramme":[{baustein,mermaid}]} → Liste (auch leer) · None bei kaputt.""" _ARTEFAKT_PROMPT = {"karteikarte": "Artefakt-Karteikarte", "beispiel": "Artefakt-Beispiel"}
if not isinstance(data, dict) or not isinstance(data.get("diagramme"), list): _ARTEFAKT_SCHRITT = {"karteikarte": "Karteikarten", "beispiel": "Beispiele"}
return None
out = []
for e in data["diagramme"]:
if isinstance(e, dict) and (m := str(e.get("mermaid", "")).strip()):
out.append({"baustein": str(e.get("baustein", "")).strip(), "mermaid": m})
return out
_ARTEFAKT_SCHEMA = {"karteikarte": _karten_schema, "beispiel": _beispiel_schema, "diagramm": _diagramm_schema}
_ARTEFAKT_PROMPT = {"karteikarte": "Artefakt-Karteikarte", "beispiel": "Artefakt-Beispiel", "diagramm": "Artefakt-Diagramm"}
_ARTEFAKT_SCHRITT = {"karteikarte": "Karteikarten", "beispiel": "Beispiele", "diagramm": "Diagramme"}
def _artefakte_komplett(files: dict) -> bool: def _artefakte_komplett(files: dict) -> bool:
@@ -1853,10 +1842,10 @@ async def _artefakte_block(ctx: GenContext, set_p, files: dict, sidecar: dict, i
async def _mirror_artefakte_db(topic: str, sidecar: dict, artefakte: dict) -> None: async def _mirror_artefakte_db(topic: str, sidecar: dict, artefakte: dict) -> None:
"""Artefakte in die DB spiegeln. Karteikarte/Beispiel je Sub (sub_norm), Diagramm je Baustein (sub_norm='').""" """Artefakte in die DB spiegeln. Karteikarte/Beispiel je Sub (sub_norm)."""
await db.delete_sub_artefakte(topic) await db.delete_sub_artefakte(topic)
btitel_list = list(sidecar.keys()) btitel_list = list(sidecar.keys())
for typ in ("karteikarte", "beispiel"): for typ in ARTEFAKT_TYPEN:
for e in artefakte.get(typ, []): for e in artefakte.get(typ, []):
bt = _match_sub(e.get("baustein", ""), btitel_list) bt = _match_sub(e.get("baustein", ""), btitel_list)
bnorm, sn = _norm_titel(bt), _norm_titel(e.get("subbaustein", "")) bnorm, sn = _norm_titel(bt), _norm_titel(e.get("subbaustein", ""))
@@ -1864,12 +1853,6 @@ async def _mirror_artefakte_db(topic: str, sidecar: dict, artefakte: dict) -> No
continue continue
daten = json.dumps({k: v for k, v in e.items() if k not in ("baustein", "subbaustein")}, ensure_ascii=False) daten = json.dumps({k: v for k, v in e.items() if k not in ("baustein", "subbaustein")}, ensure_ascii=False)
await db.put_sub_artefakt(topic, bnorm, sn, typ, daten, bt, e.get("subbaustein", "")) await db.put_sub_artefakt(topic, bnorm, sn, typ, daten, bt, e.get("subbaustein", ""))
for e in artefakte.get("diagramm", []):
bt = _match_sub(e.get("baustein", ""), btitel_list)
if not (bnorm := _norm_titel(bt)):
continue
daten = json.dumps({"mermaid": e.get("mermaid", "")}, ensure_ascii=False)
await db.put_sub_artefakt(topic, bnorm, "", "diagramm", daten, bt, "")
async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None: async def _mirror_sidecar_db(topic: str, sidecar: dict) -> None:
@@ -1912,7 +1895,7 @@ async def _mirror_frage_muster_db(topic: str, muster: dict) -> None:
async def _reset_db_ab_phase(topic: str, label: str) -> None: async def _reset_db_ab_phase(topic: str, label: str) -> None:
"""DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Artefakte).""" """DB-Inhalt der Phasen ≥ `label` verwerfen (kanonische Reihenfolge Quelle…Artefakte)."""
idx = _phase_idx(label) idx = _phase_idx(label)
if idx <= 8: # Artefakte (Karteikarten/Beispiele/Diagramme) if idx <= 8: # Artefakte (Karteikarten/Beispiele)
await db.delete_sub_artefakte(topic) await db.delete_sub_artefakte(topic)
if idx <= 7: # Fragen if idx <= 7: # Fragen
await db.delete_frage_muster(topic) await db.delete_frage_muster(topic)
@@ -2119,7 +2102,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
return # Abbruch return # Abbruch
atomic_write_json(files["frage_muster"], muster, indent=1) atomic_write_json(files["frage_muster"], muster, indent=1)
# Block F: Lern-Artefakte (Karteikarten/Beispiele/Diagramme) aus den Fakten — Bonus, # Block F: Lern-Artefakte (Karteikarten/Beispiele) aus den Fakten — Bonus,
# vom Frontend präsentiert. Bricht den Lauf nicht ab (Artefakte sind optional). # vom Frontend präsentiert. Bricht den Lauf nicht ab (Artefakte sind optional).
sidecar = _json_datei(files["sidecar"]) sidecar = _json_datei(files["sidecar"])
if _sidecar_schema(sidecar) is not None and not _artefakte_komplett(files): if _sidecar_schema(sidecar) is not None and not _artefakte_komplett(files):

View File

@@ -182,7 +182,7 @@ CREATE TABLE IF NOT EXISTS guide_gliederung (
""" """
# Generischer Lern-Artefakt-Layer: je (Baustein, Subbaustein, Typ) ein Artefakt (JSON in `daten`). # Generischer Lern-Artefakt-Layer: je (Baustein, Subbaustein, Typ) ein Artefakt (JSON in `daten`).
# typ: karteikarte | beispiel | diagramm. sub_norm='' = Baustein-Ebene (z. B. Diagramm). # typ: karteikarte | beispiel. sub_norm='' = Baustein-Ebene (für künftige Baustein-weite Artefakte).
# In der Bausteine-Phase aus den Fakten erzeugt, vom Frontend präsentiert (Trennung Artefakt/Anzeige). # In der Bausteine-Phase aus den Fakten erzeugt, vom Frontend präsentiert (Trennung Artefakt/Anzeige).
CREATE_SUB_ARTEFAKTE = """ CREATE_SUB_ARTEFAKTE = """
CREATE TABLE IF NOT EXISTS sub_artefakte ( CREATE TABLE IF NOT EXISTS sub_artefakte (
@@ -1001,7 +1001,7 @@ async def delete_gliederung(topic: str) -> None:
async def put_sub_artefakt(topic: str, baustein_norm: str, sub_norm: str, typ: str, async def put_sub_artefakt(topic: str, baustein_norm: str, sub_norm: str, typ: str,
daten: str, baustein: str = "", sub_titel: str = "") -> None: daten: str, baustein: str = "", sub_titel: str = "") -> None:
"""Ein Lern-Artefakt (karteikarte/beispiel/diagramm) als JSON in `daten` speichern.""" """Ein Lern-Artefakt (karteikarte/beispiel) als JSON in `daten` speichern."""
db = await get_db() db = await get_db()
await db.execute( await db.execute(
"""INSERT INTO sub_artefakte (topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, updated_at) """INSERT INTO sub_artefakte (topic, baustein_norm, sub_norm, typ, baustein, sub_titel, daten, updated_at)

View File

@@ -232,21 +232,18 @@ async def get_frage_muster(topic: str, baustein: str):
@router.get("/bausteine/artefakte") @router.get("/bausteine/artefakte")
async def get_artefakte(topic: str, typ: str | None = None): async def get_artefakte(topic: str, typ: str | None = None):
"""Lern-Artefakte (Karteikarten/Beispiele/Diagramme) je Thema, gruppiert nach Baustein-Norm. """Lern-Artefakte (Karteikarten/Beispiele) je Thema, gruppiert nach Baustein-Norm — je Subbaustein."""
Diagramm liegt auf Baustein-Ebene (sub_norm=''), Karteikarte/Beispiel je Subbaustein."""
rows = await get_sub_artefakte(topic, typ) rows = await get_sub_artefakte(topic, typ)
out: dict[str, dict] = {} out: dict[str, dict] = {}
for r in rows: for r in rows:
b = out.setdefault(r["baustein_norm"], {"baustein": r["baustein"], "karteikarte": [], "beispiel": [], "diagramm": None}) b = out.setdefault(r["baustein_norm"], {"baustein": r["baustein"], "karteikarte": [], "beispiel": []})
if r["baustein"] and not b["baustein"]: if r["baustein"] and not b["baustein"]:
b["baustein"] = r["baustein"] b["baustein"] = r["baustein"]
try: try:
daten = json.loads(r["daten"]) daten = json.loads(r["daten"])
except (ValueError, TypeError): except (ValueError, TypeError):
continue continue
if r["typ"] == "diagramm": if r["typ"] in ("karteikarte", "beispiel"):
b["diagramm"] = daten
elif r["typ"] in ("karteikarte", "beispiel"):
b[r["typ"]].append({"subbaustein": r["sub_titel"], **daten}) b[r["typ"]].append({"subbaustein": r["sub_titel"], **daten})
return {"artefakte": out} return {"artefakte": out}

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,6 @@
"katex": "^0.17.0", "katex": "^0.17.0",
"marked": "^18.0.4", "marked": "^18.0.4",
"marked-highlight": "^2.2.4", "marked-highlight": "^2.2.4",
"mermaid": "^11.16.0",
"vue": "^3.5.32" "vue": "^3.5.32"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,5 +1,7 @@
<script setup> <script setup>
import BausteinPanel from './BausteinPanel.vue' import BausteinPanel from './BausteinPanel.vue'
import FlashcardWidget from './FlashcardWidget.vue'
import WorkedExampleBlock from './WorkedExampleBlock.vue'
import { renderMarkdown, renderBloecke } from '../markdown.js' import { renderMarkdown, renderBloecke } from '../markdown.js'
import { stufeFuer, STUFEN } from '../stufen.js' import { stufeFuer, STUFEN } from '../stufen.js'
import { pruefeBlock, uebernehmeBlock, resetBausteinFortschritt } from '../api.js' import { pruefeBlock, uebernehmeBlock, resetBausteinFortschritt } from '../api.js'
@@ -8,6 +10,7 @@ import { useConfirm } from '../composables/useConfirm.js'
const props = defineProps({ const props = defineProps({
baustein: { type: Object, required: true }, // { title, md, num } baustein: { type: Object, required: true }, // { title, md, num }
artefakte: { type: Object, default: null }, // { karteikarte[], beispiel[], diagramm } für diesen Baustein
topic: { type: String, required: true }, topic: { type: String, required: true },
provider: { type: String, default: 'claude' }, provider: { type: String, default: 'claude' },
status: { type: Object, default: null }, status: { type: Object, default: null },
@@ -196,6 +199,10 @@ watch(() => `${props.baustein.title}|${props.baustein.md}|${props.baustein.kompa
</div> </div>
</template> </template>
</div> </div>
<template v-if="artefakte">
<WorkedExampleBlock :beispiele="artefakte.beispiel || []" />
<FlashcardWidget :karten="artefakte.karteikarte || []" />
</template>
</div> </div>
<div v-if="menu.show" class="menu-overlay" @click="menueZu" @contextmenu.prevent="menueZu"> <div v-if="menu.show" class="menu-overlay" @click="menueZu" @contextmenu.prevent="menueZu">
<div class="block-menu" :style="{ top: menu.y + 'px', left: menu.x + 'px' }" @click.stop> <div class="block-menu" :style="{ top: menu.y + 'px', left: menu.x + 'px' }" @click.stop>

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import { computed, reactive, ref, watch, nextTick, onMounted, onUnmounted } from 'vue' import { computed, reactive, ref, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { fetchGuideContent, chatGuide, fetchBausteinLernstand, fetchArtefakte } from '../api.js' import { fetchGuideContent, chatGuide, fetchBausteinLernstand, fetchArtefakte } from '../api.js'
import { renderMarkdown, renderMermaid } from '../markdown.js' import { renderMarkdown } from '../markdown.js'
import { stufeFuer, schwelle } from '../stufen.js' import { stufeFuer, schwelle } from '../stufen.js'
import { useChat } from '../composables/useChat.js' import { useChat } from '../composables/useChat.js'
import BausteinPanel from './BausteinPanel.vue' import BausteinPanel from './BausteinPanel.vue'
@@ -70,15 +70,12 @@ function setupLazy() {
if (!e.isIntersecting) continue if (!e.isIntersecting) continue
sichtbar[Number(e.target.dataset.num)] = true sichtbar[Number(e.target.dataset.num)] = true
mdObserver.unobserve(e.target) mdObserver.unobserve(e.target)
nextTick(() => renderMermaid(e.target)) // Diagramme der nun sichtbaren Section rendern
} }
}, { root: scrollEl.value, rootMargin: '800px 0px' }) }, { root: scrollEl.value, rootMargin: '800px 0px' })
for (const el of scrollEl.value.querySelectorAll('.section-card')) mdObserver.observe(el) for (const el of scrollEl.value.querySelectorAll('.section-card')) mdObserver.observe(el)
} }
watch(content, () => nextTick(setupLazy)) watch(content, () => nextTick(setupLazy))
// Artefakte kommen asynchron nach dem Content — bereits sichtbare Diagramme nachrendern.
watch(artefakte, () => nextTick(() => renderMermaid(scrollEl.value)))
onUnmounted(() => mdObserver?.disconnect()) onUnmounted(() => mdObserver?.disconnect())
watch(() => props.previewGuide?.id, loadContent, { immediate: true }) watch(() => props.previewGuide?.id, loadContent, { immediate: true })
@@ -277,6 +274,11 @@ function extractContext() {
<h1>{{ previewGuide.topic }}</h1> <h1>{{ previewGuide.topic }}</h1>
<span class="guide-format">{{ previewGuide.format }}</span> <span class="guide-format">{{ previewGuide.format }}</span>
<span v-if="themaAbgeschlossen" class="thema-done" title="Alle Bausteine auf Meister"> Thema abgeschlossen</span> <span v-if="themaAbgeschlossen" class="thema-done" title="Alle Bausteine auf Meister"> Thema abgeschlossen</span>
<span class="gh-spacer"></span>
<div class="ansicht-toggle" title="Ausführlichkeit umschalten">
<button :class="{ active: ansichtModus === 'kompakt' }" @click="$emit('setAnsicht', 'kompakt')">Kompakt</button>
<button :class="{ active: ansichtModus === 'erklärend' }" @click="$emit('setAnsicht', 'erklärend')">Erklärend</button>
</div>
</header> </header>
<section <section
@@ -303,7 +305,6 @@ function extractContext() {
<div v-if="sichtbar[s.num]" class="section-body markdown" v-html="htmlFor(s)"></div> <div v-if="sichtbar[s.num]" class="section-body markdown" v-html="htmlFor(s)"></div>
<div v-else class="section-body skeleton"></div> <div v-else class="section-body skeleton"></div>
<template v-if="sichtbar[s.num] && artefakteVon(s.title)"> <template v-if="sichtbar[s.num] && artefakteVon(s.title)">
<pre v-if="artefakteVon(s.title).diagramm" class="mermaid art-diagramm">{{ artefakteVon(s.title).diagramm.mermaid }}</pre>
<WorkedExampleBlock :beispiele="artefakteVon(s.title).beispiel || []" /> <WorkedExampleBlock :beispiele="artefakteVon(s.title).beispiel || []" />
<FlashcardWidget :karten="artefakteVon(s.title).karteikarte || []" /> <FlashcardWidget :karten="artefakteVon(s.title).karteikarte || []" />
</template> </template>
@@ -333,6 +334,7 @@ function extractContext() {
<BausteinFokus <BausteinFokus
v-if="fokusBaustein" v-if="fokusBaustein"
:baustein="fokusBaustein" :baustein="fokusBaustein"
:artefakte="artefakteVon(fokusBaustein.title)"
:topic="previewGuide.topic" :topic="previewGuide.topic"
:guide-id="previewGuide.id" :guide-id="previewGuide.id"
:provider="provider" :provider="provider"
@@ -424,12 +426,41 @@ function extractContext() {
align-items: baseline; align-items: baseline;
gap: 0.75rem; gap: 0.75rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
/* Sticky: der Ausführlichkeits-Toggle bleibt beim Scrollen erreichbar. */
position: sticky;
top: 0;
z-index: 10;
background: var(--bg-preview);
padding: 0.6rem 0 0.5rem;
h1 { h1 {
font-size: 1.7rem; font-size: 1.7rem;
} }
} }
.gh-spacer { flex: 1; }
.ansicht-toggle {
align-self: center;
display: inline-flex;
border: 1px solid var(--border);
border-radius: 8px;
overflow: hidden;
}
.ansicht-toggle button {
border: none;
background: var(--panel);
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 600;
padding: 0.3rem 0.7rem;
cursor: pointer;
}
.ansicht-toggle button.active {
background: var(--accent);
color: var(--on-accent);
}
.guide-format { .guide-format {
color: var(--text-faint); color: var(--text-faint);
font-size: 0.9rem; font-size: 0.9rem;
@@ -549,19 +580,6 @@ function extractContext() {
damit der IntersectionObserver die folgenden Karten gestaffelt erkennt. */ damit der IntersectionObserver die folgenden Karten gestaffelt erkennt. */
.section-body.skeleton { min-height: 160px; } .section-body.skeleton { min-height: 160px; }
/* Diagramm-Artefakt (Mermaid): zentriert, dezent gerahmt. Vor dem Rendern zeigt
<pre> den Quelltext — danach ersetzt mermaid.run ihn durch SVG. */
.art-diagramm {
margin: 0.75rem 0;
padding: 0.75rem;
background: var(--panel-soft);
border: 1px solid var(--border);
border-radius: 10px;
text-align: center;
overflow-x: auto;
}
.art-diagramm :deep(svg) { max-width: 100%; height: auto; }
.empty-preview { .empty-preview {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -6,17 +6,6 @@ import katex from 'katex'
import 'katex/dist/katex.min.css' import 'katex/dist/katex.min.css'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
// Mermaid lazy laden (schwere Lib): erst beim ersten Diagramm, hält das Initial-Bundle klein.
// startOnLoad:false → wir rendern gezielt; securityLevel strict: kein HTML/JS in Graphen.
let _mermaid = null
async function getMermaid() {
if (!_mermaid) {
_mermaid = (await import('mermaid')).default
_mermaid.initialize({ startOnLoad: false, securityLevel: 'strict', theme: 'default', flowchart: { htmlLabels: false } })
}
return _mermaid
}
marked.use(markedHighlight({ marked.use(markedHighlight({
langPrefix: 'hljs language-', langPrefix: 'hljs language-',
highlight(code, lang) { highlight(code, lang) {
@@ -60,33 +49,7 @@ const inlineMath = {
renderer(token) { return renderTex(token.text, false) }, renderer(token) { return renderTex(token.text, false) },
} }
// ```mermaid → <pre class="mermaid">…</pre> (Quelltext escaped; mermaid.run liest den textContent). marked.use({ extensions: [blockMath, inlineMath] })
const mermaidBlock = {
name: 'mermaidBlock',
level: 'block',
start(src) { const i = src.indexOf('```mermaid'); return i < 0 ? undefined : i },
tokenizer(src) {
const m = /^```mermaid[^\n]*\n([\s\S]+?)\n```/.exec(src)
if (m) return { type: 'mermaidBlock', raw: m[0], text: m[1] }
},
renderer(token) {
const esc = token.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
return `<pre class="mermaid">${esc}</pre>`
},
}
marked.use({ extensions: [blockMath, inlineMath, mermaidBlock] })
// Mermaid-Knoten in einem Container rendern (nach v-html). Idempotent: bereits
// verarbeitete (data-processed) überspringt mermaid.run; defekte Graphen bleiben als Text.
export async function renderMermaid(root) {
const nodes = root?.querySelectorAll?.('pre.mermaid:not([data-processed="true"])')
if (!nodes || !nodes.length) return
try {
const m = await getMermaid()
await m.run({ nodes })
} catch { /* defektes Diagramm → Quelltext bleibt sichtbar */ }
}
// Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output) // Rohes HTML im Markdown (z. B. <p>, <img> ohne Backticks aus Agenten-Output)
// als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt. // als Text anzeigen statt rendern — sonst verschluckt der Browser den Inhalt.

View File

@@ -1,24 +0,0 @@
Baue für jeden Baustein des Themas "{topic}" EIN Übersichts-Diagramm (Concept-Map) als Mermaid-Code. Es zeigt, wie die Subbausteine/Kernbegriffe des Bausteins zusammenhängen.
BAUSTEINE MIT SUBBAUSTEINEN UND IHREN FAKTEN:
{bausteine}
Regeln für das Diagramm:
- Genau EIN Mermaid-Graph je Baustein, Typ `flowchart TD` (oben→unten) oder `flowchart LR`.
- Knoten = die Subbausteine bzw. ihre Kernbegriffe. Kanten = echte Beziehungen (baut-auf, gehört-zu, führt-zu). Keine erfundenen Beziehungen.
- 38 Knoten. Lieber klar als vollständig. Stütze dich auf Voraussetzungen/Kernpunkte aus den Fakten.
- **Gültige Mermaid-Syntax, sonst rendert nichts:**
- Knoten-IDs sind kurze ASCII-Wörter ohne Leerzeichen (z. B. `dfa`, `nfa1`).
- Label in eckigen Klammern, Text schlicht: `dfa[Deterministischer Automat]`. KEINE Anführungszeichen, runden Klammern, Doppelpunkte oder `#` im Label.
- Kanten mit `-->` (optional Label: `a -->|baut auf| b`).
- Zeilen im `mermaid`-Feld mit `\n` trennen (ein JSON-String).
- Lässt sich ein Baustein nicht sinnvoll als Graph zeigen (ein einziger Begriff), LASSE IHN WEG.
Schreibe die Diagramme als EIN JSON in die Datei {out_path} (nutze dein Schreib-Werkzeug), GENAU so:
{{"diagramme": [
{{"baustein": "<exakter Baustein-Titel>",
"mermaid": "flowchart TD\n a[Alphabet] --> b[Wort]\n b --> c[Sprache]"}}
]}}
Gib sonst keinen Text aus.
{extra}