update
This commit is contained in:
@@ -546,7 +546,7 @@ async def set_baustein_absolviert(topic: str, baustein: str) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def count_relevant_subs(topic: str, baustein: str) -> int:
|
async def count_relevant_subs(topic: str, baustein: str) -> int:
|
||||||
"""Anzahl relevanter Konsens-Subbausteine eines Bausteins (für den cap = 2×Subs)."""
|
"""Anzahl relevanter Konsens-Subbausteine eines Bausteins (für den cap = 4×Subs)."""
|
||||||
from textkit import _norm_titel
|
from textkit import _norm_titel
|
||||||
db = await get_db()
|
db = await get_db()
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import math
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
from agents import run_agent
|
from agents import run_agent
|
||||||
from bausteine import _pdfs_konvertieren, quelle_ordner
|
from bausteine import _pdfs_konvertieren, quelle_ordner
|
||||||
from config import (
|
from config import (
|
||||||
@@ -22,9 +24,9 @@ from config import (
|
|||||||
LESBARKEIT_AKTIV, TEMPLATES_DIR,
|
LESBARKEIT_AKTIV, TEMPLATES_DIR,
|
||||||
)
|
)
|
||||||
import lesbarkeit
|
import lesbarkeit
|
||||||
from database import list_guides, update_guide, list_bausteine, list_subbausteine, set_guide_content
|
from database import list_guides, update_guide, list_bausteine, list_subbausteine, set_guide_content, get_guide_content
|
||||||
from fsutil import atomic_write_json, atomic_write_text
|
from fsutil import atomic_write_json, atomic_write_text
|
||||||
from jsonio import read_json_file as _json_datei
|
from jsonio import read_json_file as _json_datei, parse_json_text as _parse_json_text
|
||||||
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
|
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
CANCELLED, FAILED, GenContext, _claude_error, _extra,
|
CANCELLED, FAILED, GenContext, _claude_error, _extra,
|
||||||
@@ -816,3 +818,88 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
await _fail(guide_id, str(e)[:2000])
|
await _fail(guide_id, str(e)[:2000])
|
||||||
finally:
|
finally:
|
||||||
clear_guide_cancelled(guide_id)
|
clear_guide_cancelled(guide_id)
|
||||||
|
|
||||||
|
|
||||||
|
# --- On-Demand: eine Section prüfen / beheben / neu schreiben (Fokus, interaktiv) ---
|
||||||
|
|
||||||
|
SECTION_PRUEFEN_TIMEOUT = 300
|
||||||
|
SECTION_FIX_TIMEOUT = 600
|
||||||
|
|
||||||
|
|
||||||
|
def _section_spec() -> str:
|
||||||
|
return (TEMPLATES_DIR / "Format" / "Section.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _section_facts(topic: str) -> str:
|
||||||
|
project = quelle_ordner(topic)
|
||||||
|
return _prompt("Guide-Fakten-Projekt", project=project) if project else _prompt("Guide-Fakten-Thema")
|
||||||
|
|
||||||
|
|
||||||
|
def _hinweis_block(hinweis: str) -> str:
|
||||||
|
hinweis = (hinweis or "").strip()
|
||||||
|
return f"HINWEIS DES NUTZERS (besonders beachten):\n{hinweis}" if hinweis else ""
|
||||||
|
|
||||||
|
|
||||||
|
async def _subs_text(topic: str, baustein: str) -> str:
|
||||||
|
"""Relevante Subbausteine eines Bausteins mit Stufe — als Checkliste für die Agenten."""
|
||||||
|
subs_raw = await _load_subbausteine(topic)
|
||||||
|
subs = [s for s in subs_raw.get(_titel(baustein), []) if s.get("relevanz") != "rand"]
|
||||||
|
if not subs:
|
||||||
|
return "(keine Subbausteine hinterlegt — 3–7 knappe Punkte abdecken)"
|
||||||
|
return "\n".join(f"- [{s['stufe']}] {s['titel']}" for s in subs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _guide_content_laden(topic: str, format_name: str) -> dict | None:
|
||||||
|
js = await get_guide_content(topic, format_name)
|
||||||
|
if not js:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return json.loads(js)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _section_finden(content: dict, baustein: str) -> dict | None:
|
||||||
|
for ch in content.get("chapters", []):
|
||||||
|
for s in ch.get("sections", []):
|
||||||
|
if s.get("title") == baustein:
|
||||||
|
return s
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def block_pruefen(topic: str, format_name: str, baustein: str, stelle: str, block: str, hinweis: str = "", provider: str = DEFAULT_PROVIDER) -> str | None:
|
||||||
|
"""Einen Abschnitt (Markdown-Block) gegen die Guide-Regeln prüfen → korrigierte
|
||||||
|
Block-Version als Markdown. None = Fehler/Section fehlt."""
|
||||||
|
content = await _guide_content_laden(topic, format_name)
|
||||||
|
sec = _section_finden(content, baustein) if content else None
|
||||||
|
if sec is None:
|
||||||
|
return None
|
||||||
|
ganze = sec.get("kompakt", "") if str(stelle).startswith("kompakt") else sec.get("md", "")
|
||||||
|
prompt = _prompt(
|
||||||
|
"Block-Pruefen", topic=topic, spec=_section_spec(), facts=_section_facts(topic),
|
||||||
|
subbausteine=await _subs_text(topic, baustein), kontext=ganze, block=block, hinweis=_hinweis_block(hinweis),
|
||||||
|
)
|
||||||
|
rc, stdout, _ = await run_agent(
|
||||||
|
f"block-pruefen-{uuid.uuid4()}", prompt, SECTION_PRUEFEN_TIMEOUT,
|
||||||
|
provider=provider, role="judge", capabilities="none", lane="interactive",
|
||||||
|
)
|
||||||
|
neu = stdout.strip() if rc == 0 else ""
|
||||||
|
return neu or None
|
||||||
|
|
||||||
|
|
||||||
|
async def block_uebernehmen(topic: str, format_name: str, baustein: str, stelle: str, alt: str, neu: str) -> dict | None:
|
||||||
|
"""Einen Block (alt→neu) im Feld kompakt/ausführlich ersetzen + persistieren.
|
||||||
|
→ {kompakt, md, gefunden}; None = Section fehlt."""
|
||||||
|
content = await _guide_content_laden(topic, format_name)
|
||||||
|
sec = _section_finden(content, baustein) if content else None
|
||||||
|
if sec is None:
|
||||||
|
return None
|
||||||
|
feld = "kompakt" if str(stelle).startswith("kompakt") else "md"
|
||||||
|
aktuell = sec.get(feld, "") or ""
|
||||||
|
gefunden = alt in aktuell
|
||||||
|
if gefunden:
|
||||||
|
sec[feld] = aktuell.replace(alt, neu, 1)
|
||||||
|
js = json.dumps(content, ensure_ascii=False)
|
||||||
|
await set_guide_content(topic, format_name, js)
|
||||||
|
atomic_write_json(guide_content_path(topic, format_name), content, indent=1)
|
||||||
|
return {"kompakt": sec.get("kompakt", ""), "md": sec.get("md", ""), "gefunden": gefunden}
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ STUFEN = (("anfaenger", 0.2), ("fortgeschritten", 0.4), ("experte", 0.6), ("meis
|
|||||||
|
|
||||||
|
|
||||||
def cap_fuer(n_subs: int) -> int:
|
def cap_fuer(n_subs: int) -> int:
|
||||||
"""Maximal-Score eines Bausteins: 2 Punkte je relevantem Subbaustein, gedeckelt [10, 50]."""
|
"""Maximal-Score eines Bausteins: 4 Punkte je relevantem Subbaustein, gedeckelt [10, 50]."""
|
||||||
return max(10, min(50, 2 * n_subs))
|
return max(10, min(50, 4 * n_subs))
|
||||||
|
|
||||||
|
|
||||||
def _schwelle(p: float, cap: int) -> int:
|
def _schwelle(p: float, cap: int) -> int:
|
||||||
@@ -84,7 +84,7 @@ def score_berechnen(basis: int, delta: int, floor: int, cap: int) -> int:
|
|||||||
"""Neuer Score · driftfrei aus der Basis. `delta` liefert die Route je Prüfungsform
|
"""Neuer Score · driftfrei aus der Basis. `delta` liefert die Route je Prüfungsform
|
||||||
(Quiz +1/+3, Lückentext +1/+3, Erklären −1…+3; falsch = progressiver_malus). Auf
|
(Quiz +1/+3, Lückentext +1/+3, Erklären −1…+3; falsch = progressiver_malus). Auf
|
||||||
[floor, cap] geklemmt. floor = höchste erreichte Stufenschwelle (kein Rückfall);
|
[floor, cap] geklemmt. floor = höchste erreichte Stufenschwelle (kein Rückfall);
|
||||||
cap = 2× relevante Subbausteine [10, 50]. Am Deckel (basis ≥ cap) eingefroren („Max")."""
|
cap = 4× relevante Subbausteine [10, 50]. Am Deckel (basis ≥ cap) eingefroren („Max")."""
|
||||||
if basis >= cap:
|
if basis >= cap:
|
||||||
return basis
|
return basis
|
||||||
return max(floor, min(cap, basis + delta))
|
return max(floor, min(cap, basis + delta))
|
||||||
|
|||||||
@@ -230,8 +230,8 @@ class BausteinPruefungRequest(BaseModel):
|
|||||||
nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt
|
nachgefragt: bool = False # für diese Frage wurde nachgefragt → Gewinn auf +1 gedeckelt
|
||||||
begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden
|
begruendung: str = Field(default="", max_length=2000) # „Gründlich prüfen": warum mit der Bewertung unzufrieden
|
||||||
muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback)
|
muster: str = Field(default="", max_length=2000) # gezogenes Frage-Muster (Saat); leer → Live-Generierung (Fallback)
|
||||||
# Basis + cap werden serverseitig geführt (Anker bzw. 2×Subs) — Client-cap nur Hinweis.
|
# Basis + cap werden serverseitig geführt (Anker bzw. 4×Subs) — Client-cap nur Hinweis.
|
||||||
cap: int = Field(default=10, ge=1, le=50) # Score-Deckel = clamp(2×relevante Subbausteine, 10, 50)
|
cap: int = Field(default=10, ge=1, le=50) # Score-Deckel = clamp(4×relevante Subbausteine, 10, 50)
|
||||||
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
messages: list[ChatMessage] = [] # Dialog bisher; leer = erste Frage
|
||||||
provider: ProviderType = "claude"
|
provider: ProviderType = "claude"
|
||||||
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
gruendlich: bool = False # „Gründlich prüfen": Bewertung mit starkem Modell (role guide)
|
||||||
@@ -253,14 +253,42 @@ class BausteinPruefungResponse(BaseModel):
|
|||||||
loesung: str | None = None # Lückentext: erwarteter Begriff
|
loesung: str | None = None # Lückentext: erwarteter Begriff
|
||||||
alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme
|
alternativen: list[str] | None = None # Lückentext: akzeptierte Synonyme
|
||||||
gute_antworten: int
|
gute_antworten: int
|
||||||
cap: int = 10 # Maximal-Score dieses Bausteins (2×Subs, geklemmt) — Frontend leitet die Stufe ab
|
cap: int = 10 # Maximal-Score dieses Bausteins (4×Subs, geklemmt) — Frontend leitet die Stufe ab
|
||||||
|
|
||||||
|
|
||||||
class BausteinLernstand(BaseModel):
|
class BausteinLernstand(BaseModel):
|
||||||
gute_antworten: int
|
gute_antworten: int
|
||||||
streak: int = 0
|
streak: int = 0
|
||||||
cap: int = 10 # 2×relevante Subbausteine, geklemmt [10, 50]
|
cap: int = 10 # 4×relevante Subbausteine, geklemmt [10, 50]
|
||||||
|
|
||||||
|
|
||||||
class BausteinLernstandResponse(BaseModel):
|
class BausteinLernstandResponse(BaseModel):
|
||||||
bausteine: dict[str, BausteinLernstand]
|
bausteine: dict[str, BausteinLernstand]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Block-Inhalt: einen Abschnitt on-demand prüfen + übernehmen (Fokus, Rechtsklick) ---
|
||||||
|
|
||||||
|
class BlockPruefenRequest(BaseModel):
|
||||||
|
baustein: str = Field(min_length=1, max_length=200)
|
||||||
|
stelle: str = "ausführlich" # "kompakt" | "ausführlich" (angezeigtes Feld)
|
||||||
|
block: str = Field(min_length=1, max_length=20000) # roher Markdown-Block
|
||||||
|
hinweis: str = Field(default="", max_length=2000) # optionaler Zusatz (✏️)
|
||||||
|
provider: ProviderType = "claude"
|
||||||
|
|
||||||
|
|
||||||
|
class BlockPruefenResponse(BaseModel):
|
||||||
|
neu: str # korrigierter Block als Markdown
|
||||||
|
|
||||||
|
|
||||||
|
class BlockUebernehmenRequest(BaseModel):
|
||||||
|
baustein: str = Field(min_length=1, max_length=200)
|
||||||
|
stelle: str = "ausführlich"
|
||||||
|
alt: str = Field(min_length=1, max_length=20000)
|
||||||
|
neu: str = Field(default="", max_length=20000)
|
||||||
|
provider: ProviderType = "claude"
|
||||||
|
|
||||||
|
|
||||||
|
class BlockUebernehmenResponse(BaseModel):
|
||||||
|
kompakt: str
|
||||||
|
md: str
|
||||||
|
gefunden: bool
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ async def lade_lernstand() -> tuple[list[dict], dict[str, set[str]], dict[str, d
|
|||||||
"""Guides + Kapitel-Fortschritt + Bausteine je Stufe.
|
"""Guides + Kapitel-Fortschritt + Bausteine je Stufe.
|
||||||
|
|
||||||
levels: {"anfaenger"/"fortgeschritten"/"experte"/"meister": {topic → normalisierte Titel}}.
|
levels: {"anfaenger"/"fortgeschritten"/"experte"/"meister": {topic → normalisierte Titel}}.
|
||||||
Stufe je Baustein wird aus Score + cap (2×relevante Subs) abgeleitet.
|
Stufe je Baustein wird aus Score + cap (4×relevante Subs) abgeleitet.
|
||||||
"""
|
"""
|
||||||
scores = await list_baustein_scores_all()
|
scores = await list_baustein_scores_all()
|
||||||
caps = await subs_count_alle()
|
caps = await subs_count_alle()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from database import (
|
|||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, lade_frage_muster
|
||||||
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
from elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
||||||
from lernen import NIVEAUS, cap_fuer, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_score, progressiver_malus, stufe_aus_score
|
from lernen import NIVEAUS, cap_fuer, baustein_chat, baustein_diskussion, baustein_element_anlegen, pruefung_bewertung, pruefung_bewertung_schnell, pruefung_frage, pruefung_frage_variante, quiz_generieren, lueckwahl_generieren, lueckentext_generieren, lueckentext_pruefen, score_berechnen, floor_aus_score, progressiver_malus, stufe_aus_score
|
||||||
from guide import generate_guide, guide_slot_dateien, guide_fertig_step
|
from guide import generate_guide, guide_slot_dateien, guide_fertig_step, block_pruefen, block_uebernehmen
|
||||||
from pipeline import cancel_guide
|
from pipeline import cancel_guide
|
||||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||||
from models import (
|
from models import (
|
||||||
@@ -35,6 +35,7 @@ from models import (
|
|||||||
FolderResponse, BausteineQuelleUpdate, BausteineQuelleResponse, BausteinUebersicht,
|
FolderResponse, BausteineQuelleUpdate, BausteineQuelleResponse, BausteinUebersicht,
|
||||||
BausteinChatRequest, BausteinChatResponse,
|
BausteinChatRequest, BausteinChatResponse,
|
||||||
BausteinPruefungRequest, BausteinPruefungResponse, BausteinLernstandResponse,
|
BausteinPruefungRequest, BausteinPruefungResponse, BausteinLernstandResponse,
|
||||||
|
BlockPruefenRequest, BlockPruefenResponse, BlockUebernehmenRequest, BlockUebernehmenResponse,
|
||||||
)
|
)
|
||||||
from paths import bausteine_topics, guide_content_path, project_dir, topic_dir, quelle_path, safe_ordner
|
from paths import bausteine_topics, guide_content_path, project_dir, topic_dir, quelle_path, safe_ordner
|
||||||
from fsutil import atomic_write_json
|
from fsutil import atomic_write_json
|
||||||
@@ -221,7 +222,7 @@ async def get_frage_muster(topic: str, baustein: str):
|
|||||||
|
|
||||||
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
@router.get("/bausteine/lernstand", response_model=BausteinLernstandResponse)
|
||||||
async def baustein_lernstand(topic: str):
|
async def baustein_lernstand(topic: str):
|
||||||
"""Prüfungs-Stand pro Baustein (roher Titel als Key). cap = 2×relevante Subs [10,50]
|
"""Prüfungs-Stand pro Baustein (roher Titel als Key). cap = 4×relevante Subs [10,50]
|
||||||
— für ALLE Bausteine (auch noch nicht geprüfte), damit der cap sofort stimmt."""
|
— für ALLE Bausteine (auch noch nicht geprüfte), damit der cap sofort stimmt."""
|
||||||
progress = {p["baustein"]: p for p in await list_baustein_progress(topic)}
|
progress = {p["baustein"]: p for p in await list_baustein_progress(topic)}
|
||||||
caps = await subs_count_roh(topic)
|
caps = await subs_count_roh(topic)
|
||||||
@@ -281,7 +282,7 @@ def _erklaeren_delta(niveau: str, nachgefragt: bool) -> int:
|
|||||||
|
|
||||||
|
|
||||||
async def _cap_fuer(topic: str, baustein: str) -> int:
|
async def _cap_fuer(topic: str, baustein: str) -> int:
|
||||||
"""Maximal-Score des Bausteins = clamp(2×relevante Subbausteine, 10, 50)."""
|
"""Maximal-Score des Bausteins = clamp(4×relevante Subbausteine, 10, 50)."""
|
||||||
return cap_fuer(await count_relevant_subs(topic, baustein))
|
return cap_fuer(await count_relevant_subs(topic, baustein))
|
||||||
|
|
||||||
|
|
||||||
@@ -517,6 +518,31 @@ async def guide_chat(guide_id: str, req: GuideChatRequest):
|
|||||||
return {"reply": reply}
|
return {"reply": reply}
|
||||||
|
|
||||||
|
|
||||||
|
async def _guide_tf(guide_id: str) -> tuple[str, str]:
|
||||||
|
guide = await get_guide(guide_id)
|
||||||
|
if guide is None:
|
||||||
|
raise HTTPException(404, "Guide nicht gefunden")
|
||||||
|
return guide["topic"], guide["format"]
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/guides/{guide_id}/block/pruefen", response_model=BlockPruefenResponse)
|
||||||
|
async def block_pruefen_route(guide_id: str, req: BlockPruefenRequest):
|
||||||
|
topic, fmt = await _guide_tf(guide_id)
|
||||||
|
neu = await block_pruefen(topic, fmt, req.baustein, req.stelle, req.block, req.hinweis, provider=req.provider)
|
||||||
|
if neu is None:
|
||||||
|
raise HTTPException(502, "Prüfung fehlgeschlagen — bitte erneut versuchen")
|
||||||
|
return {"neu": neu}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/guides/{guide_id}/block/uebernehmen", response_model=BlockUebernehmenResponse)
|
||||||
|
async def block_uebernehmen_route(guide_id: str, req: BlockUebernehmenRequest):
|
||||||
|
topic, fmt = await _guide_tf(guide_id)
|
||||||
|
res = await block_uebernehmen(topic, fmt, req.baustein, req.stelle, req.alt, req.neu)
|
||||||
|
if res is None:
|
||||||
|
raise HTTPException(404, "Section nicht gefunden")
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
# --- Elemente (persönliche Zusammenfassung) ---
|
# --- Elemente (persönliche Zusammenfassung) ---
|
||||||
|
|
||||||
@router.get("/elements", response_model=list[ElementResponse])
|
@router.get("/elements", response_model=list[ElementResponse])
|
||||||
|
|||||||
@@ -152,6 +152,24 @@ export async function fetchGuideContent(id) {
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Einen Markdown-Block on-demand gegen die Guide-Regeln prüfen (Fokus, Rechtsklick).
|
||||||
|
export async function pruefeBlock(id, { baustein, stelle, block, hinweis = '', provider }) {
|
||||||
|
const res = await fetch(`${BASE}/guides/${id}/block/pruefen`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ baustein, stelle, block, hinweis, provider }),
|
||||||
|
})
|
||||||
|
return jsonOrThrow(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geprüften Block persistent übernehmen (alt → neu im jeweiligen Feld).
|
||||||
|
export async function uebernehmeBlock(id, { baustein, stelle, alt, neu, provider }) {
|
||||||
|
const res = await fetch(`${BASE}/guides/${id}/block/uebernehmen`, {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ baustein, stelle, alt, neu, provider }),
|
||||||
|
})
|
||||||
|
return jsonOrThrow(res)
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchTopics() {
|
export async function fetchTopics() {
|
||||||
const res = await fetch(`${BASE}/topics`)
|
const res = await fetch(`${BASE}/topics`)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|||||||
@@ -7,11 +7,22 @@
|
|||||||
margin: 0 0 0.5em;
|
margin: 0 0 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KaTeX: lange Block-Formeln scrollen statt das Layout zu sprengen */
|
/* KaTeX: lange Block-Formeln scrollen statt das Layout zu sprengen.
|
||||||
|
Sichtbarer dünner Scrollbalken (statt unsichtbarem Overlay) → man erkennt,
|
||||||
|
dass die Formel weitergeht und nicht abgeschnitten ist. */
|
||||||
.markdown .katex-display {
|
.markdown .katex-display {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
padding: 0.2em 0;
|
padding: 0.2em 0 0.45em;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--border) transparent;
|
||||||
|
}
|
||||||
|
.markdown .katex-display::-webkit-scrollbar {
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
.markdown .katex-display::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border);
|
||||||
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown p:last-child {
|
.markdown p:last-child {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import BausteinPanel from './BausteinPanel.vue'
|
import BausteinPanel from './BausteinPanel.vue'
|
||||||
import { renderMarkdown } from '../markdown.js'
|
import { renderMarkdown, renderBloecke } from '../markdown.js'
|
||||||
import { stufeFuer, STUFEN } from '../stufen.js'
|
import { stufeFuer, STUFEN } from '../stufen.js'
|
||||||
|
import { pruefeBlock, uebernehmeBlock } from '../api.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
baustein: { type: Object, required: true }, // { title, md, num }
|
baustein: { type: Object, required: true }, // { title, md, num }
|
||||||
@@ -9,6 +10,7 @@ const props = defineProps({
|
|||||||
provider: { type: String, default: 'claude' },
|
provider: { type: String, default: 'claude' },
|
||||||
status: { type: Object, default: null },
|
status: { type: Object, default: null },
|
||||||
cap: { type: Number, default: 6 },
|
cap: { type: Number, default: 6 },
|
||||||
|
guideId: { type: String, default: '' },
|
||||||
tab: { type: String, default: 'pruefung' },
|
tab: { type: String, default: 'pruefung' },
|
||||||
ansicht: { type: String, default: 'kompakt' }, // kompakt | erklärend — linke Guide-Spalte
|
ansicht: { type: String, default: 'kompakt' }, // kompakt | erklärend — linke Guide-Spalte
|
||||||
hasPrev: { type: Boolean, default: false },
|
hasPrev: { type: Boolean, default: false },
|
||||||
@@ -16,9 +18,9 @@ const props = defineProps({
|
|||||||
fortschritt: { type: Object, default: () => ({ total: 0, anfaenger: 0, fortgeschritten: 0, experte: 0, meister: 0 }) },
|
fortschritt: { type: Object, default: () => ({ total: 0, anfaenger: 0, fortgeschritten: 0, experte: 0, meister: 0 }) },
|
||||||
})
|
})
|
||||||
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht', 'openSidebar'])
|
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht', 'openSidebar', 'sectionUpdated'])
|
||||||
|
|
||||||
const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
|
const guideEl = ref(null) // linke Guide-Spalte (Scroll-Ziel für ALT+↑/↓)
|
||||||
const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
|
const rightEl = ref(null) // rechte Spalte (Prüfungs-Panel mit Eingabefeld)
|
||||||
@@ -68,6 +70,58 @@ const stufe = computed(() => {
|
|||||||
const s = props.status || {}
|
const s = props.status || {}
|
||||||
return stufeFuer(s.gute_antworten || 0, s.cap || props.cap)
|
return stufeFuer(s.gute_antworten || 0, s.cap || props.cap)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// --- Block-Prüfen (Rechtsklick auf einen Abschnitt) ---
|
||||||
|
const angezeigterText = computed(() =>
|
||||||
|
props.ansicht === 'kompakt' ? (props.baustein.kompakt || props.baustein.md) : props.baustein.md,
|
||||||
|
)
|
||||||
|
const bloecke = computed(() => renderBloecke(angezeigterText.value))
|
||||||
|
// Feld, in das übernommen wird (muss zum angezeigten Text passen).
|
||||||
|
const stelle = computed(() => (props.ansicht === 'kompakt' && props.baustein.kompakt) ? 'kompakt' : 'ausführlich')
|
||||||
|
|
||||||
|
const menu = reactive({ show: false, x: 0, y: 0, index: null })
|
||||||
|
function blockMenue(i, e) { menu.show = true; menu.x = e.clientX; menu.y = e.clientY; menu.index = i }
|
||||||
|
function menueZu() { menu.show = false }
|
||||||
|
|
||||||
|
// Vorschlag je Block-Index: { neu, laeuft, fehler, editOffen, hinweis }
|
||||||
|
const vorschlaege = reactive({})
|
||||||
|
|
||||||
|
async function blockPruefen(i, zusatz = '') {
|
||||||
|
const raw = bloecke.value[i]?.raw
|
||||||
|
if (!raw || !props.guideId) return
|
||||||
|
menueZu()
|
||||||
|
vorschlaege[i] = { neu: '', laeuft: true, fehler: '', editOffen: false, hinweis: '' }
|
||||||
|
try {
|
||||||
|
const res = await pruefeBlock(props.guideId, { baustein: props.baustein.title, stelle: stelle.value, block: raw, hinweis: zusatz, provider: props.provider })
|
||||||
|
vorschlaege[i] = { neu: res.neu, laeuft: false, fehler: '', editOffen: false, hinweis: '' }
|
||||||
|
} catch (e) {
|
||||||
|
vorschlaege[i] = { neu: '', laeuft: false, fehler: e.message || 'Prüfung fehlgeschlagen', editOffen: false, hinweis: '' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function blockUebernehmen(i) {
|
||||||
|
const v = vorschlaege[i]; const raw = bloecke.value[i]?.raw
|
||||||
|
if (!v || v.laeuft || !raw) return
|
||||||
|
v.laeuft = true; v.fehler = ''
|
||||||
|
try {
|
||||||
|
const res = await uebernehmeBlock(props.guideId, { baustein: props.baustein.title, stelle: stelle.value, alt: raw, neu: v.neu, provider: props.provider })
|
||||||
|
if (res.gefunden) emit('sectionUpdated', { title: props.baustein.title, kompakt: res.kompakt, md: res.md })
|
||||||
|
else { v.laeuft = false; v.fehler = 'Stelle nicht gefunden — evtl. schon geändert.' }
|
||||||
|
} catch (e) { v.laeuft = false; v.fehler = e.message || 'Übernehmen fehlgeschlagen' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function blockVerwerfen(i) { delete vorschlaege[i] }
|
||||||
|
function blockEdit(i) { const v = vorschlaege[i]; if (v) v.editOffen = !v.editOffen }
|
||||||
|
function blockEditSenden(i) {
|
||||||
|
const z = (vorschlaege[i]?.hinweis || '').trim()
|
||||||
|
if (z) blockPruefen(i, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baustein/Inhalt-Wechsel → Vorschläge + Menü zurücksetzen (Block-Indizes neu).
|
||||||
|
watch(() => `${props.baustein.title}|${props.baustein.md}|${props.baustein.kompakt || ''}`, () => {
|
||||||
|
for (const k of Object.keys(vorschlaege)) delete vorschlaege[k]
|
||||||
|
menueZu()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -91,7 +145,32 @@ const stufe = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="fokus-body">
|
<div class="fokus-body">
|
||||||
<div ref="guideEl" class="fokus-col left">
|
<div ref="guideEl" class="fokus-col left">
|
||||||
<div class="markdown" v-html="renderMarkdown(ansicht === 'kompakt' ? (baustein.kompakt || baustein.md) : baustein.md)"></div>
|
<div class="markdown">
|
||||||
|
<template v-for="(b, i) in bloecke" :key="i">
|
||||||
|
<div class="md-block" v-html="b.html" @contextmenu.prevent="blockMenue(i, $event)"></div>
|
||||||
|
<div v-if="vorschlaege[i]" class="block-vorschlag">
|
||||||
|
<div v-if="vorschlaege[i].laeuft" class="bv-status">Prüfe Abschnitt…</div>
|
||||||
|
<template v-else>
|
||||||
|
<p v-if="vorschlaege[i].fehler" class="bv-fehler">{{ vorschlaege[i].fehler }}</p>
|
||||||
|
<div class="markdown bv-neu" v-html="renderMarkdown(vorschlaege[i].neu)"></div>
|
||||||
|
<div class="bv-aktionen">
|
||||||
|
<button class="bv-btn ja" title="Übernehmen" @click="blockUebernehmen(i)">✓</button>
|
||||||
|
<button class="bv-btn" title="Verwerfen" @click="blockVerwerfen(i)">✗</button>
|
||||||
|
<button class="bv-btn" :class="{ aktiv: vorschlaege[i].editOffen }" title="Hinweis ergänzen" @click="blockEdit(i)">✏️</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="vorschlaege[i].editOffen" class="bv-edit">
|
||||||
|
<input v-model="vorschlaege[i].hinweis" class="bv-input" placeholder="Zusatz-Info → erneut prüfen" @keyup.enter="blockEditSenden(i)" />
|
||||||
|
<button class="bv-btn ja" title="Erneut prüfen" @click="blockEditSenden(i)">↻</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
<button class="bm-item" @click="blockPruefen(menu.index)">Prüfen</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="rightEl" class="fokus-col right">
|
<div ref="rightEl" class="fokus-col right">
|
||||||
<BausteinPanel
|
<BausteinPanel
|
||||||
@@ -217,4 +296,68 @@ const stufe = computed(() => {
|
|||||||
.fokus-col { overflow-y: visible; }
|
.fokus-col { overflow-y: visible; }
|
||||||
.fokus-col.right { width: auto; }
|
.fokus-col.right { width: auto; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Block-Prüfen: Rechtsklick-Menü + Vorschlag unter dem Abschnitt */
|
||||||
|
.md-block { border-radius: 6px; transition: background 0.15s; }
|
||||||
|
.md-block:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
|
||||||
|
|
||||||
|
.menu-overlay { position: fixed; inset: 0; z-index: 60; }
|
||||||
|
.block-menu {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 8rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
.bm-item {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.4rem 0.7rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bm-item:hover { background: color-mix(in srgb, var(--accent) 14%, transparent); }
|
||||||
|
|
||||||
|
.block-vorschlag {
|
||||||
|
margin: 0.5rem 0 1rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: color-mix(in srgb, var(--accent) 6%, var(--panel));
|
||||||
|
}
|
||||||
|
.bv-status { font-size: 0.9rem; color: var(--text-soft, #888); }
|
||||||
|
.bv-fehler { color: var(--danger, #dc2626); font-size: 0.88rem; margin: 0 0 0.5rem; }
|
||||||
|
.bv-neu > :first-child { margin-top: 0; }
|
||||||
|
.bv-neu > :last-child { margin-bottom: 0; }
|
||||||
|
.bv-aktionen { display: flex; gap: 0.4rem; margin-top: 0.6rem; }
|
||||||
|
.bv-btn {
|
||||||
|
min-width: 2rem; height: 2rem; padding: 0 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.bv-btn:hover { border-color: var(--accent); }
|
||||||
|
.bv-btn.aktiv { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--panel)); }
|
||||||
|
.bv-btn.ja { border-color: var(--success-border); color: var(--success); }
|
||||||
|
.bv-edit { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
|
||||||
|
.bv-input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['statusChanged', 'openFokus', 'setAnsicht'])
|
const emit = defineEmits(['statusChanged', 'openFokus', 'setAnsicht'])
|
||||||
|
|
||||||
// Lernstufen relativ zum cap (stufen.js): grün/blau/lila/gold @ 20/40/60/100 %.
|
// Lernstufen relativ zum cap (stufen.js): grün/blau/lila/gold @ 20/40/60/100 %.
|
||||||
// Prüfung IMMER zufällig aus 5 Formen (mit Muster); cap = 2×relevante Subbausteine.
|
// Prüfung IMMER zufällig aus 5 Formen (mit Muster); cap = 4×relevante Subbausteine.
|
||||||
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
||||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, cap: props.cap })
|
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, cap: props.cap })
|
||||||
const score = computed(() => st.value.gute_antworten || 0)
|
const score = computed(() => st.value.gute_antworten || 0)
|
||||||
|
|||||||
@@ -95,6 +95,19 @@ function onBausteinStatus(baustein, status) {
|
|||||||
if (neu !== alt) emit('progressChanged') // Stufenwechsel → Locks/Stats neu laden
|
if (neu !== alt) emit('progressChanged') // Stufenwechsel → Locks/Stats neu laden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Section nach Prüfen/Beheben/Neu-Schreiben im Content ersetzen + Markdown-Cache leeren.
|
||||||
|
function onSectionUpdated({ title, kompakt, md }) {
|
||||||
|
if (!content.value) return
|
||||||
|
for (const ch of content.value.chapters) {
|
||||||
|
for (const s of ch.sections) {
|
||||||
|
if (s.title !== title) continue
|
||||||
|
s.md = md
|
||||||
|
s.kompakt = kompakt
|
||||||
|
for (const k of [...mdCache.keys()]) if (k.endsWith(`:${s.num}`)) mdCache.delete(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Vollbild-Fokus: ein Baustein groß, Guide links + Prüfung rechts ---
|
// --- Vollbild-Fokus: ein Baustein groß, Guide links + Prüfung rechts ---
|
||||||
const fokusIndex = ref(null) // Index in der flachen Baustein-Liste; null = zu
|
const fokusIndex = ref(null) // Index in der flachen Baustein-Liste; null = zu
|
||||||
const fokusTab = ref('pruefung')
|
const fokusTab = ref('pruefung')
|
||||||
@@ -137,7 +150,7 @@ const fortschritt = computed(() => {
|
|||||||
return z
|
return z
|
||||||
})
|
})
|
||||||
|
|
||||||
// cap je Baustein = 2×relevante Subbausteine (vom Backend in lernstand[title].cap geliefert).
|
// cap je Baustein = 4×relevante Subbausteine (vom Backend in lernstand[title].cap geliefert).
|
||||||
function capVon(title) {
|
function capVon(title) {
|
||||||
return lernstand.value[title]?.cap || 10
|
return lernstand.value[title]?.cap || 10
|
||||||
}
|
}
|
||||||
@@ -287,6 +300,7 @@ function extractContext() {
|
|||||||
v-if="fokusBaustein"
|
v-if="fokusBaustein"
|
||||||
:baustein="fokusBaustein"
|
:baustein="fokusBaustein"
|
||||||
:topic="previewGuide.topic"
|
:topic="previewGuide.topic"
|
||||||
|
:guide-id="previewGuide.id"
|
||||||
:provider="provider"
|
:provider="provider"
|
||||||
:fortschritt="fortschritt"
|
:fortschritt="fortschritt"
|
||||||
:status="lernstand[fokusBaustein.title]"
|
:status="lernstand[fokusBaustein.title]"
|
||||||
@@ -301,6 +315,7 @@ function extractContext() {
|
|||||||
@set-ansicht="$emit('setAnsicht', $event)"
|
@set-ansicht="$emit('setAnsicht', $event)"
|
||||||
@status-changed="(st) => onBausteinStatus(st.baustein, st)"
|
@status-changed="(st) => onBausteinStatus(st.baustein, st)"
|
||||||
@open-sidebar="$emit('openSidebar')"
|
@open-sidebar="$emit('openSidebar')"
|
||||||
|
@section-updated="onSectionUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button v-if="previewGuide && !chatOpen && fokusIndex === null" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
|
<button v-if="previewGuide && !chatOpen && fokusIndex === null" class="chat-fab" :class="{ shifted: elementsOpen }" title="Fragen zum Guide" @click="openChat">💬</button>
|
||||||
|
|||||||
@@ -71,3 +71,12 @@ export function renderMarkdown(text) {
|
|||||||
export function renderMarkdownInline(text) {
|
export function renderMarkdownInline(text) {
|
||||||
return DOMPurify.sanitize(marked.parseInline(text || ''))
|
return DOMPurify.sanitize(marked.parseInline(text || ''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Markdown in Top-Level-Blöcke zerlegen: je Block { raw (exakte Quelle), html }.
|
||||||
|
// raw ist verlustfrei (tokens.map(raw).join('') === Original) → Block-genaues Ersetzen.
|
||||||
|
export function renderBloecke(text) {
|
||||||
|
const tokens = marked.lexer(text || '')
|
||||||
|
return tokens
|
||||||
|
.filter((t) => t.type !== 'space' && (t.raw || '').trim())
|
||||||
|
.map((t) => ({ raw: t.raw, html: DOMPurify.sanitize(marked.parser([t])) }))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Lernstufen je Baustein — identisch zum Backend (lernen.py STUFEN).
|
// Lernstufen je Baustein — identisch zum Backend (lernen.py STUFEN).
|
||||||
// Floor in % des cap (cap = clamp(2×relevante Subbausteine, 10, 50)).
|
// Floor in % des cap (cap = clamp(4×relevante Subbausteine, 10, 50)).
|
||||||
// grün=Anfänger 20% · blau=Fortgeschritten 40% · lila=Experte 60% · gold=Meister 100%.
|
// grün=Anfänger 20% · blau=Fortgeschritten 40% · lila=Experte 60% · gold=Meister 100%.
|
||||||
export const STUFEN = [
|
export const STUFEN = [
|
||||||
{ key: 'anfaenger', label: 'Anfänger', kurz: '✓', floor: 0.2, farbe: 'var(--stufe-anfaenger)' },
|
{ key: 'anfaenger', label: 'Anfänger', kurz: '✓', floor: 0.2, farbe: 'var(--stufe-anfaenger)' },
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ Markdown im Section-Body: erklärende Absätze in normalem Text, Aufzählungen a
|
|||||||
|
|
||||||
Mathematik IMMER als LaTeX schreiben: inline zwischen `$…$` (z. B. `$\Sigma^*$`, `$L \subseteq U$`, `$k = 3$`), abgesetzte Formeln zwischen `$$…$$`. KEINE Unicode-Sonderzeichen als Mathe-Ersatz (nicht `x₁`, `¬`, `∨`, `≤` — stattdessen `$x_1$`, `$\neg$`, `$\lor$`, `$\le$`) und keine nackten Formeln ohne `$`. Außerhalb von Mathe normaler Text.
|
Mathematik IMMER als LaTeX schreiben: inline zwischen `$…$` (z. B. `$\Sigma^*$`, `$L \subseteq U$`, `$k = 3$`), abgesetzte Formeln zwischen `$$…$$`. KEINE Unicode-Sonderzeichen als Mathe-Ersatz (nicht `x₁`, `¬`, `∨`, `≤` — stattdessen `$x_1$`, `$\neg$`, `$\lor$`, `$\le$`) und keine nackten Formeln ohne `$`. Außerhalb von Mathe normaler Text.
|
||||||
|
|
||||||
|
Formeln brechen NICHT automatisch um — lange Mathe läuft sonst über den Rand. Darum:
|
||||||
|
- **Inline `$…$` nur für KURZE Symbole/Terme** — einzelne Variablen, Mengen, kurze Relationen (`$\Sigma^*$`, `$k = 3$`, `$x \notin L$`). NIE einen ganzen Ausdruck mit mehreren Teilen oder Sätzen inline (auch nicht in einem Listenpunkt).
|
||||||
|
- **Lange oder mehrteilige Formeln IMMER abgesetzt zwischen `$$…$$` auf eigener Zeile** — besonders Mengen-/Set-Builder-Definitionen (`\{ … \mid … \}`). Nicht inline, nicht in eine Aufzählung quetschen.
|
||||||
|
- **KEINE Prosa in `\text{…}`.** Bedingungen und Erklärungen als normalen deutschen Text NEBEN oder UNTER die Formel, nicht in sie hinein. Also nicht `$$L = \text{CLIQUE} = \{ u\#v \mid u \text{ kodiert die Adjazenzmatrix …}\}$$`, sondern die Formel knapp (`$$L = \{\, u\#v \mid \dots \,\}$$`) und die Bedeutung von `u`, `v` im Fließtext erklären.
|
||||||
|
- Lange Definitionen oder Gleichungsketten mit `$$\begin{aligned} … \\ … \end{aligned}$$` über mehrere Zeilen umbrechen.
|
||||||
|
|
||||||
Beispiel einer fertigen Section (Code-Thema, nur der Body):
|
Beispiel einer fertigen Section (Code-Thema, nur der Body):
|
||||||
|
|
||||||
Arrays lösen ein simples Problem: Du willst viele Werte unter einem Namen halten, statt für jeden eine eigene Variable. In PHP gibt es zwei Sorten. Indizierte Arrays nummerieren die Werte durch (`[0 => 'a']`). Assoziative Arrays geben jedem Wert einen eigenen Schlüssel (`['key' => 'wert']`) — praktisch, wenn die Position egal ist, der Name aber zählt. Intern sind beide dasselbe: geordnete Hashmaps.
|
Arrays lösen ein simples Problem: Du willst viele Werte unter einem Namen halten, statt für jeden eine eigene Variable. In PHP gibt es zwei Sorten. Indizierte Arrays nummerieren die Werte durch (`[0 => 'a']`). Assoziative Arrays geben jedem Wert einen eigenen Schlüssel (`['key' => 'wert']`) — praktisch, wenn die Position egal ist, der Name aber zählt. Intern sind beide dasselbe: geordnete Hashmaps.
|
||||||
|
|||||||
25
templates/Prompt/Block-Pruefen.md
Normal file
25
templates/Prompt/Block-Pruefen.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
Prüfe EINEN Abschnitt (Markdown-Block) einer Section eines Lern-Guides zum Thema "{topic}" gegen die Guide-Regeln und gib ihn KORRIGIERT zurück. Zielgruppe: Anfänger ohne Vorwissen.
|
||||||
|
|
||||||
|
{facts}
|
||||||
|
|
||||||
|
SECTION-SPEZIFIKATION (Soll-Zustand):
|
||||||
|
{spec}
|
||||||
|
|
||||||
|
SUBBAUSTEINE der Section (Kontext):
|
||||||
|
{subbausteine}
|
||||||
|
|
||||||
|
GANZE SECTION (nur Kontext — NICHT mit ausgeben):
|
||||||
|
{kontext}
|
||||||
|
|
||||||
|
ZU PRÜFENDER ABSCHNITT:
|
||||||
|
{block}
|
||||||
|
|
||||||
|
{hinweis}
|
||||||
|
|
||||||
|
Prüfe den Abschnitt auf echte Mängel und behebe sie — was in Ordnung ist, bleibt unverändert:
|
||||||
|
- Fachliche Fehler, falsche oder erfundene Fakten/Werte.
|
||||||
|
- Verständlichkeit für Anfänger; Lesbarkeit (Sätze über ~25 Wörter, Schachtelsätze, Textwand, eine Aufzählung als Fließtext-Satz, unerklärte Fachbegriffe).
|
||||||
|
- Beispiel passend und korrekt, Format laut Spezifikation.
|
||||||
|
- Formeln laut Spezifikation (lange/mehrteilige als abgesetzte `$$…$$`, keine Prosa in `\text{{…}}`); sauberes Markdown.
|
||||||
|
|
||||||
|
Gib NUR den korrigierten Abschnitt als Markdown zurück — kein weiterer Text, keine Erklärung, keine Marker. Ist nichts zu ändern, gib den Abschnitt unverändert zurück.
|
||||||
Reference in New Issue
Block a user