update
This commit is contained in:
@@ -27,12 +27,12 @@ from lernen import FRAGETYPEN
|
|||||||
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
from paths import arbeit_dir, bausteine_path, frage_muster_path, project_dir, subbausteine_path, quelle_path, quelle_crawl_dir, safe_ordner
|
||||||
from crawl import crawl
|
from crawl import crawl
|
||||||
from pipeline import (
|
from pipeline import (
|
||||||
CANCELLED, FAILED, GenContext, _extra, _gather_fortschritt, _log, _prompt, _race, _relevanz_schema, _rest_schema,
|
CANCELLED, FAILED, GenContext, _extra, _gather_fortschritt, _log, _prompt, _race, _relevanz_schema,
|
||||||
_runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot,
|
_runde_schema, _semaphore, _str_liste, _stufen_schema, _timeout, run_single_slot,
|
||||||
)
|
)
|
||||||
from textkit import (
|
from textkit import (
|
||||||
_eindeutige_titel, _lade_bausteine, _norm_titel, _parse_auswahl, _parse_subbausteine, _titel,
|
_eindeutige_titel, _lade_bausteine, _norm_titel, _parse_auswahl, _parse_subbausteine, _titel,
|
||||||
_titel_aufloesen, _titel_index, _vormerge,
|
_titel_aufloesen, _titel_index,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Subbausteine (Websuche je Baustein) chunken: 1 Agent je ~10 Bausteine, gedeckelt.
|
# Subbausteine (Websuche je Baustein) chunken: 1 Agent je ~10 Bausteine, gedeckelt.
|
||||||
@@ -490,31 +490,13 @@ def _relevanz_komplett(data) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _frage_muster_schema(data) -> list[dict] | None:
|
|
||||||
"""{"muster": [{subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None.
|
|
||||||
|
|
||||||
typ muss ein bekannter Fragetyp sein; subbaustein + frage nicht leer. Leere Liste → None.
|
|
||||||
"""
|
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
|
||||||
return None
|
|
||||||
out = []
|
|
||||||
for e in data["muster"]:
|
|
||||||
if not isinstance(e, dict):
|
|
||||||
return None
|
|
||||||
sub = str(e.get("subbaustein", "")).strip()
|
|
||||||
typ = str(e.get("typ", "")).strip().casefold()
|
|
||||||
frage = str(e.get("frage", "")).strip()
|
|
||||||
if not sub or typ not in FRAGETYPEN or not frage:
|
|
||||||
return None
|
|
||||||
out.append({"subbaustein": sub, "typ": typ, "frage": frage})
|
|
||||||
return out or None
|
|
||||||
|
|
||||||
|
|
||||||
def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
def _frage_muster_chunk_schema(data) -> list[dict] | None:
|
||||||
"""{"muster": [{baustein, subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None.
|
"""{"muster": [{baustein, subbaustein, typ, frage}, …]} → Liste valider Einträge · sonst None.
|
||||||
|
|
||||||
Wie _frage_muster_schema, aber mit `baustein` (Zuordnung im 10er-Chunk). Ungültige
|
Mit `baustein` (Zuordnung im 10er-Chunk). Ungültige Einzel-Einträge werden
|
||||||
Einzel-Einträge werden übersprungen (nicht die ganze Liste verworfen)."""
|
übersprungen (nicht die ganze Liste verworfen)."""
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
if not isinstance(data, dict) or not isinstance(data.get("muster"), list):
|
||||||
return None
|
return None
|
||||||
out = []
|
out = []
|
||||||
@@ -567,54 +549,10 @@ def _lpt_chunks(gewichte: list[int], target: int) -> list[list[int]]:
|
|||||||
return [b for b in bins if b]
|
return [b for b in bins if b]
|
||||||
|
|
||||||
|
|
||||||
def _merge_finder(num: int, idx: dict, finder: list[dict]) -> tuple[list[str], list[str]]:
|
|
||||||
"""Subbausteine eines Bausteins über die Finder mergen → (konsens ≥2, rest ==1)."""
|
|
||||||
counts: dict[str, int] = {}
|
|
||||||
repr_text: dict[str, str] = {}
|
|
||||||
order: list[str] = []
|
|
||||||
for d in finder:
|
|
||||||
# die Marker-Liste dieses Finders für genau diesen Baustein
|
|
||||||
subs = next((s for marker, s in d.items() if _titel_aufloesen(idx, marker) == num), [])
|
|
||||||
gesehen: set[str] = set()
|
|
||||||
for sub in subs:
|
|
||||||
key = _norm_titel(sub)
|
|
||||||
if not key or key in gesehen:
|
|
||||||
continue
|
|
||||||
gesehen.add(key)
|
|
||||||
if key not in counts:
|
|
||||||
counts[key], repr_text[key] = 0, sub
|
|
||||||
order.append(key)
|
|
||||||
counts[key] += 1
|
|
||||||
konsens = [repr_text[k] for k in order if counts[k] >= 2]
|
|
||||||
rest = [repr_text[k] for k in order if counts[k] == 1]
|
|
||||||
return konsens, rest
|
|
||||||
|
|
||||||
|
|
||||||
def _final_text(chunk: list[int], entries: dict, daten: dict) -> str:
|
|
||||||
"""Marker-Datei aus reinem Konsens (wenn kein Judge nötig)."""
|
|
||||||
teile = []
|
|
||||||
for num in chunk:
|
|
||||||
konsens = daten[num][0]
|
|
||||||
if konsens:
|
|
||||||
teile.append(f"<!-- baustein: {_titel(entries[num])} -->\n" + "\n".join(f"- {s}" for s in konsens))
|
|
||||||
return "\n".join(teile) + "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def _judge_block(chunk: list[int], entries: dict, daten: dict) -> str:
|
|
||||||
"""Eingabe für den Subbaustein-Judge: pro Baustein Konsens + Strittiges."""
|
|
||||||
lines = []
|
|
||||||
for num in chunk:
|
|
||||||
konsens, rest = daten[num]
|
|
||||||
lines.append(f"BAUSTEIN: {_titel(entries[num])}")
|
|
||||||
lines.append("Konsens:")
|
|
||||||
lines.extend(f"- {s}" for s in konsens)
|
|
||||||
if not konsens:
|
|
||||||
lines.append("- (noch keiner)")
|
|
||||||
if rest:
|
|
||||||
lines.append("Strittig:")
|
|
||||||
lines.extend(f"- {s}" for s in rest)
|
|
||||||
lines.append("")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict | None:
|
async def _subbausteine_block(ctx: GenContext, set_p, files: dict, entries: dict, instructions: str) -> dict | None:
|
||||||
@@ -1191,14 +1129,6 @@ async def _frage_muster_block(ctx: GenContext, set_p, files: dict, sidecar: dict
|
|||||||
|
|
||||||
# ── Inventar in der DB: Recherche-Loop · Konsolidierung · Klärung ────────────
|
# ── Inventar in der DB: Recherche-Loop · Konsolidierung · Klärung ────────────
|
||||||
|
|
||||||
def _cited_sources(text: str) -> set[str]:
|
|
||||||
"""Zitierte Quellen je Eintrag = 3. ' — '-Segment (URL bzw. Dateiname), kleingeschrieben."""
|
|
||||||
out = set()
|
|
||||||
for eintrag in _parse_auswahl(text).values():
|
|
||||||
teile = [t.strip() for t in eintrag.split(" — ")]
|
|
||||||
if len(teile) >= 3 and teile[-1]:
|
|
||||||
out.add(teile[-1].lower())
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _crawl_index(ordner) -> dict[str, str]:
|
def _crawl_index(ordner) -> dict[str, str]:
|
||||||
@@ -1221,29 +1151,8 @@ def _crawl_index(ordner) -> dict[str, str]:
|
|||||||
return idx
|
return idx
|
||||||
|
|
||||||
|
|
||||||
def _abgedeckt(zitiert: set[str], crawl_idx: dict[str, str]) -> set[str]:
|
|
||||||
"""Zitierte Quellen → Menge kanonischer Crawl-Seiten-Keys (was nicht passt, fällt weg)."""
|
|
||||||
out = set()
|
|
||||||
for z in zitiert:
|
|
||||||
key = crawl_idx.get(z) or crawl_idx.get(z.rstrip("/"))
|
|
||||||
if key:
|
|
||||||
out.add(key)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _fokus_text(bereits: list[str], offen: list[str]) -> str:
|
|
||||||
"""Re-Prompt-Block: noch nicht abgedeckte Crawl-Seiten + bereits gefundene Titel."""
|
|
||||||
teile = []
|
|
||||||
if offen:
|
|
||||||
liste = "\n".join(f"- {n}" for n in offen[:150])
|
|
||||||
teile.append(
|
|
||||||
"NOCH NICHT ABGEDECKTE QUELLEN-DATEIEN — lies ZUERST genau diese im Quell-Ordner "
|
|
||||||
f"und ergänze daraus die noch fehlenden Bausteine:\n{liste}"
|
|
||||||
)
|
|
||||||
if bereits:
|
|
||||||
liste = "\n".join(f"- {t}" for t in bereits)
|
|
||||||
teile.append("BEREITS GEFUNDEN (NICHT wiederholen — liefere nur NEUE Bausteine):\n" + liste)
|
|
||||||
return ("\n\n" + "\n\n".join(teile)) if teile else ""
|
|
||||||
|
|
||||||
|
|
||||||
async def _set_inventar(topic: str, eintrag: str, status: str) -> None:
|
async def _set_inventar(topic: str, eintrag: str, status: str) -> None:
|
||||||
|
|||||||
@@ -492,17 +492,6 @@ async def set_baustein_score(topic: str, baustein: str, score: int) -> int:
|
|||||||
return score
|
return score
|
||||||
|
|
||||||
|
|
||||||
async def set_baustein_streak(topic: str, baustein: str, streak: int) -> None:
|
|
||||||
"""Persistiert die Streak-Folge (Bonus) absolut — überlebt Reload/Session-Wechsel."""
|
|
||||||
db = await get_db()
|
|
||||||
await db.execute(
|
|
||||||
"""INSERT INTO baustein_progress (topic, baustein, streak, updated_at)
|
|
||||||
VALUES (?, ?, ?, ?)
|
|
||||||
ON CONFLICT(topic, baustein) DO UPDATE SET
|
|
||||||
streak = excluded.streak, updated_at = excluded.updated_at""",
|
|
||||||
(topic, baustein, streak, _now()),
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
async def set_baustein_verstanden(topic: str, baustein: str) -> bool:
|
async def set_baustein_verstanden(topic: str, baustein: str) -> bool:
|
||||||
@@ -793,11 +782,6 @@ async def get_step_status(topic: str, schritt: str) -> str:
|
|||||||
return row[0] if row else "offen"
|
return row[0] if row else "offen"
|
||||||
|
|
||||||
|
|
||||||
async def list_pipeline_state(topic: str) -> dict[str, str]:
|
|
||||||
db = await get_db()
|
|
||||||
cursor = await db.execute("SELECT schritt, status FROM pipeline_state WHERE topic = ?", (topic,))
|
|
||||||
rows = await cursor.fetchall()
|
|
||||||
return {s: st for s, st in rows}
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_pipeline_state(topic: str, schritte: list[str] | None = None) -> None:
|
async def delete_pipeline_state(topic: str, schritte: list[str] | None = None) -> None:
|
||||||
@@ -810,24 +794,8 @@ async def delete_pipeline_state(topic: str, schritte: list[str] | None = None) -
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
async def set_quelle(topic: str, type: str, ort: str = "", spec: str = "") -> None:
|
|
||||||
db = await get_db()
|
|
||||||
await db.execute(
|
|
||||||
"""INSERT INTO quelle (topic, type, ort, spec, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
||||||
ON CONFLICT(topic) DO UPDATE SET
|
|
||||||
type = excluded.type, ort = excluded.ort, spec = excluded.spec, updated_at = excluded.updated_at""",
|
|
||||||
(topic, type, ort, spec, _now()),
|
|
||||||
)
|
|
||||||
await db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
async def get_quelle(topic: str) -> dict | None:
|
|
||||||
db = await get_db()
|
|
||||||
cursor = await db.execute("SELECT type, ort, spec FROM quelle WHERE topic = ?", (topic,))
|
|
||||||
row = await cursor.fetchone()
|
|
||||||
if row is None:
|
|
||||||
return None
|
|
||||||
return {"type": row[0], "ort": row[1], "spec": row[2]}
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_quelle(topic: str) -> None:
|
async def delete_quelle(topic: str) -> None:
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ async def _load_subbausteine(topic: str) -> dict[str, list[dict]]:
|
|||||||
|
|
||||||
|
|
||||||
def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str:
|
def _zuteilung_subs(chunk: list[dict], entries: dict[int, str], subs_by_titel: dict[str, list[dict]]) -> str:
|
||||||
"""Wie _zuteilung_text, aber listet pro Baustein seine Subbausteine mit Stufe."""
|
"""Listet je Kapitel die Bausteine, darunter ihre Subbausteine mit Stufe."""
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
for ch in chunk:
|
for ch in chunk:
|
||||||
lines.append(f"KAPITEL: {ch['title']}")
|
lines.append(f"KAPITEL: {ch['title']}")
|
||||||
|
|||||||
@@ -116,11 +116,6 @@ def _str_liste(val) -> list[str] | None:
|
|||||||
return None if any(not x for x in out) else out
|
return None if any(not x for x in out) else out
|
||||||
|
|
||||||
|
|
||||||
def _rest_schema(data):
|
|
||||||
"""{"uebernehmen": [str]} → Liste (leer erlaubt) · sonst None."""
|
|
||||||
if not isinstance(data, dict):
|
|
||||||
return None
|
|
||||||
return _str_liste(data.get("uebernehmen"))
|
|
||||||
|
|
||||||
|
|
||||||
def _runde_schema(data, final: bool = False):
|
def _runde_schema(data, final: bool = False):
|
||||||
|
|||||||
@@ -54,11 +54,6 @@ def _content_json(topic: str, fmt: str) -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _kapitel_titel(topic: str, fmt: str) -> set[str] | None:
|
|
||||||
content = _content_json(topic, fmt)
|
|
||||||
if content is None:
|
|
||||||
return None
|
|
||||||
return {c.get("title") for c in content.get("chapters", [])}
|
|
||||||
|
|
||||||
|
|
||||||
def _section_titel(topic: str, fmt: str) -> set[str] | None:
|
def _section_titel(topic: str, fmt: str) -> set[str] | None:
|
||||||
|
|||||||
@@ -42,28 +42,6 @@ def _eindeutige_titel(entries: dict[int, str]) -> dict[int, str]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _vormerge(listen: list[dict[int, str]]) -> list[tuple[str, int]]:
|
|
||||||
"""Vereinigt Baustein-Listen: exakte Titel-Dubletten (per _norm_titel) zusammenführen.
|
|
||||||
|
|
||||||
→ [("Titel — Beschreibung", nennungen)] in Erstnennungs-Reihenfolge.
|
|
||||||
nennungen = Anzahl der Listen, die den Titel nennen (Dublette innerhalb
|
|
||||||
einer Liste zählt nicht doppelt). Repräsentant ist die Erstnennung; ein
|
|
||||||
drittes " — "-Segment (Quelle) wird verworfen.
|
|
||||||
"""
|
|
||||||
merged: dict[str, tuple[str, int]] = {}
|
|
||||||
for liste in listen:
|
|
||||||
gesehen: set[str] = set()
|
|
||||||
for text in liste.values():
|
|
||||||
key = _norm_titel(_titel(text))
|
|
||||||
if key in gesehen:
|
|
||||||
continue
|
|
||||||
gesehen.add(key)
|
|
||||||
if key in merged:
|
|
||||||
repr_text, n = merged[key]
|
|
||||||
merged[key] = (repr_text, n + 1)
|
|
||||||
else:
|
|
||||||
merged[key] = (" — ".join(text.split(" — ")[:2]), 1)
|
|
||||||
return list(merged.values())
|
|
||||||
|
|
||||||
|
|
||||||
def _titel_index(entries: dict[int, str]) -> dict[str, int]:
|
def _titel_index(entries: dict[int, str]) -> dict[str, int]:
|
||||||
@@ -231,9 +209,3 @@ def _split_chunks(chapters: list[dict], n: int) -> list[list[dict]]:
|
|||||||
return chunks
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
def _zuteilung_text(chunk: list[dict], entries: dict[int, str]) -> str:
|
|
||||||
lines = []
|
|
||||||
for ch in chunk:
|
|
||||||
lines.append(f"KAPITEL: {ch['title']}")
|
|
||||||
lines.extend(f"- {entries[num]}" for num in ch["nums"])
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const selectedTopic = ref(null)
|
|||||||
const previewGuide = ref(null)
|
const previewGuide = ref(null)
|
||||||
const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false')
|
const sidebarPinned = ref(localStorage.getItem('sidebarPinned') !== 'false')
|
||||||
const sidebarSticky = ref(false)
|
const sidebarSticky = ref(false)
|
||||||
|
const fokusOffen = ref(false) // Baustein-Vollbild aktiv → Sidebar als Overlay über das Fokus-Overlay heben
|
||||||
const darkMode = ref(
|
const darkMode = ref(
|
||||||
localStorage.getItem('darkMode') === null
|
localStorage.getItem('darkMode') === null
|
||||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
@@ -374,9 +375,9 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky }">
|
<div class="layout" :class="{ 'sidebar-floating': !sidebarPinned, 'sidebar-open': sidebarSticky, 'sidebar-over-fokus': fokusOffen && sidebarSticky }">
|
||||||
<div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
|
<div v-if="!sidebarPinned" class="hover-zone" @click="clickHoverZone"></div>
|
||||||
<div v-if="!sidebarPinned && sidebarSticky" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
<div v-if="(!sidebarPinned && sidebarSticky) || (fokusOffen && sidebarSticky)" class="sidebar-backdrop" @click="sidebarSticky = false"></div>
|
||||||
<TopicSidebar
|
<TopicSidebar
|
||||||
:topics="topics"
|
:topics="topics"
|
||||||
:selectedTopic="selectedTopic"
|
:selectedTopic="selectedTopic"
|
||||||
@@ -441,6 +442,8 @@ onMounted(async () => {
|
|||||||
:ansichtModus="ansichtModus"
|
:ansichtModus="ansichtModus"
|
||||||
@progressChanged="loadStats(); loadBausteine()"
|
@progressChanged="loadStats(); loadBausteine()"
|
||||||
@setAnsicht="setAnsicht"
|
@setAnsicht="setAnsicht"
|
||||||
|
@open-sidebar="sidebarSticky = true"
|
||||||
|
@fokus-active="fokusOffen = $event"
|
||||||
/>
|
/>
|
||||||
<div v-else class="empty-main">
|
<div v-else class="empty-main">
|
||||||
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
<p>Thema in der Sidebar anlegen oder auswählen.</p>
|
||||||
@@ -583,6 +586,21 @@ textarea::placeholder {
|
|||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Baustein-Vollbild: Sidebar als Overlay ÜBER das Fokus-Overlay (z-index 40) heben.
|
||||||
|
Greift für gepinnt UND floating. */
|
||||||
|
.layout.sidebar-over-fokus > .sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 100dvh;
|
||||||
|
transform: translateX(0);
|
||||||
|
z-index: 50;
|
||||||
|
box-shadow: 0 0 16px var(--shadow);
|
||||||
|
}
|
||||||
|
.layout.sidebar-over-fokus .sidebar-backdrop {
|
||||||
|
z-index: 49;
|
||||||
|
}
|
||||||
|
|
||||||
.empty-main {
|
.empty-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -114,11 +114,6 @@ export async function fetchProviders() {
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchProjects() {
|
|
||||||
const res = await fetch(`${BASE}/projects`)
|
|
||||||
return res.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchFolders(kind) {
|
export async function fetchFolders(kind) {
|
||||||
const res = await fetch(`${BASE}/folders?kind=${encodeURIComponent(kind)}`)
|
const res = await fetch(`${BASE}/folders?kind=${encodeURIComponent(kind)}`)
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
@@ -143,10 +138,6 @@ export async function fetchBausteineUebersicht(topic) {
|
|||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteProject(name) {
|
|
||||||
await fetch(`${BASE}/projects/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function cancelGuide(id) {
|
export async function cancelGuide(id) {
|
||||||
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
|
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht'])
|
const emit = defineEmits(['close', 'prev', 'next', 'statusChanged', 'setAnsicht', 'openSidebar'])
|
||||||
|
|
||||||
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)
|
||||||
@@ -72,6 +72,7 @@ const standFarbe = computed(() => {
|
|||||||
<div class="fokus-overlay" :style="{ '--stand': standFarbe }">
|
<div class="fokus-overlay" :style="{ '--stand': standFarbe }">
|
||||||
<div class="fokus-bar">
|
<div class="fokus-bar">
|
||||||
<div class="fokus-bar-inner">
|
<div class="fokus-bar-inner">
|
||||||
|
<button class="fokus-btn" title="Navigation öffnen" @click="$emit('openSidebar')">☰</button>
|
||||||
<button class="fokus-btn" :disabled="!hasPrev" title="Voriger Baustein" @click="$emit('prev')">←</button>
|
<button class="fokus-btn" :disabled="!hasPrev" title="Voriger Baustein" @click="$emit('prev')">←</button>
|
||||||
<button class="fokus-btn" :disabled="!hasNext" title="Nächster Baustein" @click="$emit('next')">→</button>
|
<button class="fokus-btn" :disabled="!hasNext" title="Nächster Baustein" @click="$emit('next')">→</button>
|
||||||
<span class="fokus-titel">{{ baustein.title }}</span>
|
<span class="fokus-titel">{{ baustein.title }}</span>
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ const VERSTANDEN = 12
|
|||||||
const GEMEISTERT = 18
|
const GEMEISTERT = 18
|
||||||
const VOLL = 30
|
const VOLL = 30
|
||||||
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false })
|
const st = computed(() => props.status || { gute_antworten: 0, streak: 0, absolviert: false, verstanden: false, gemeistert: false })
|
||||||
// Aktiver Modus aus Score-Band (unter 30 fix) + Schwierigkeit (ALT+E). Ab 30 frei.
|
// Volle Prüfung (cap 30): durchgehend zufällig aus allen 5 Formen. Kurze Prüfung (cap 6):
|
||||||
// Ohne Frage-Muster (alter Baustein) gibt es nur Erklären — Quiz/Lückentext brauchen die Saat.
|
// Score-Band + Schwierigkeit (ALT+E). Ohne Frage-Muster (alter Baustein) nur Erklären.
|
||||||
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
const MODI = ['quiz_leicht', 'quiz_schwer', 'lueck_leicht', 'lueck_schwer', 'erklaeren']
|
||||||
const score = computed(() => st.value.gute_antworten || 0)
|
const score = computed(() => st.value.gute_antworten || 0)
|
||||||
const schwerWahl = ref(false) // leicht/schwer in den Bändern unter 19 (ALT+E)
|
const schwerWahl = ref(false) // leicht/schwer in den Bändern unter 19 (ALT+E)
|
||||||
@@ -42,9 +42,13 @@ function bandFuer(s) {
|
|||||||
}
|
}
|
||||||
// Am Format-Max: Score eingefroren („Max"). Gilt auch bei cap 30 (FullGuide, endgültig).
|
// Am Format-Max: Score eingefroren („Max"). Gilt auch bei cap 30 (FullGuide, endgültig).
|
||||||
const amDeckel = computed(() => score.value >= props.cap)
|
const amDeckel = computed(() => score.value >= props.cap)
|
||||||
// Aktuelles Band: am Deckel bleibt das Band des erreichten Formats (cap−1), sonst score-basiert.
|
// Aktuelles Band: volle Prüfung (cap 30) ist DURCHGEHEND zufällig aus allen 5 Formen.
|
||||||
|
// Sonst score-basiert (am Deckel das Band des erreichten Formats, cap−1).
|
||||||
// Ohne Frage-Muster (alter Baustein) gibt es nur Erklären.
|
// Ohne Frage-Muster (alter Baustein) gibt es nur Erklären.
|
||||||
const aktuellesBand = computed(() => musterModus.value ? bandFuer(amDeckel.value ? props.cap - 1 : score.value) : 'erklaeren')
|
const aktuellesBand = computed(() =>
|
||||||
|
!musterModus.value ? 'erklaeren'
|
||||||
|
: props.cap >= VOLL ? 'zufall'
|
||||||
|
: bandFuer(amDeckel.value ? props.cap - 1 : score.value))
|
||||||
// 'zufall' = Platzhalter; das gezogene Objekt trägt die echte Form/Schwierigkeit.
|
// 'zufall' = Platzhalter; das gezogene Objekt trägt die echte Form/Schwierigkeit.
|
||||||
const aktiverModus = computed(() => {
|
const aktiverModus = computed(() => {
|
||||||
const b = aktuellesBand.value
|
const b = aktuellesBand.value
|
||||||
@@ -74,10 +78,10 @@ const aktSchwer = computed(() => {
|
|||||||
return schwer.value
|
return schwer.value
|
||||||
})
|
})
|
||||||
const modusRegel = computed(() => {
|
const modusRegel = computed(() => {
|
||||||
const m = imZufall.value ? '−3' : '−1' // Zufallband: Fehler kostet −3
|
const m = score.value >= GEMEISTERT ? '−3' : '−1' // Fehler kostet erst ab Score 18 −3
|
||||||
if (zeigeForm.value === 'quiz') return aktSchwer.value ? `x von 4 · +3/${m}` : `1 von 4 · +1/${m}`
|
if (zeigeForm.value === 'quiz') return aktSchwer.value ? `x von 4 · +3/${m}` : `1 von 4 · +1/${m}`
|
||||||
if (zeigeForm.value === 'lueckentext') return aktSchwer.value ? `frei tippen · +3/${m}` : `Begriff aus 4 · +1/${m}`
|
if (zeigeForm.value === 'lueckentext') return aktSchwer.value ? `frei tippen · +3/${m}` : `Begriff aus 4 · +1/${m}`
|
||||||
return imZufall.value ? '+1/+2/+3 · Fehler −3' : '+1/+2/+3 · Nachfragen max +1 · schwach −1'
|
return score.value >= GEMEISTERT ? '+1/+2/+3 · Fehler −3' : '+1/+2/+3 · Nachfragen max +1 · schwach −1'
|
||||||
})
|
})
|
||||||
// Ende des aktuellen Bands (nächster Meilenstein).
|
// Ende des aktuellen Bands (nächster Meilenstein).
|
||||||
const bandZiel = computed(() => {
|
const bandZiel = computed(() => {
|
||||||
@@ -689,7 +693,9 @@ function onPruefKey(e) {
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
<p class="bp-hint">
|
<p class="bp-hint">
|
||||||
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — <strong>Max</strong>.</template>
|
<template v-if="amDeckel">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} — <strong>Max</strong>.</template>
|
||||||
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}<span v-if="imZufall" class="bp-hint-key"> · zufällig</span></template>
|
<template v-else-if="imZufall && !anzeigeForm">Zufällig · {{ score }}/{{ cap }}</template>
|
||||||
|
<template v-else-if="imZufall">{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ cap }} · {{ modusRegel }}</template>
|
||||||
|
<template v-else>{{ FORM_NAME[zeigeForm] }} · {{ score }}/{{ bandZiel }} · {{ modusRegel }}</template>
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
v-if="!amDeckel && !imZufall && zeigeForm !== 'erklaeren'"
|
v-if="!amDeckel && !imZufall && zeigeForm !== 'erklaeren'"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const props = defineProps({
|
|||||||
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
ansichtModus: { type: String, default: 'kompakt' }, // kompakt | erklärend
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['progressChanged', 'setAnsicht'])
|
const emit = defineEmits(['progressChanged', 'setAnsicht', 'openSidebar', 'fokusActive'])
|
||||||
|
|
||||||
// Rotierende Kapitel-Akzentfarben (ohne Rot)
|
// Rotierende Kapitel-Akzentfarben (ohne Rot)
|
||||||
const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366f1']
|
const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366f1']
|
||||||
@@ -97,6 +97,10 @@ const bausteine = computed(() =>
|
|||||||
)
|
)
|
||||||
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
|
const fokusBaustein = computed(() => (fokusIndex.value === null ? null : bausteine.value[fokusIndex.value]))
|
||||||
|
|
||||||
|
// Fokus-Zustand an App melden (für die Sidebar über dem Overlay); Themenwechsel schließt den Fokus.
|
||||||
|
watch(fokusIndex, (v) => emit('fokusActive', v !== null))
|
||||||
|
watch(() => props.previewGuide?.id, () => { fokusIndex.value = null })
|
||||||
|
|
||||||
function openFokus(baustein, tab) {
|
function openFokus(baustein, tab) {
|
||||||
if (!istPruefbar(baustein)) return // reine Lese-Sections (Rest/Rand) öffnen keinen Prüfungs-Fokus
|
if (!istPruefbar(baustein)) return // reine Lese-Sections (Rest/Rand) öffnen keinen Prüfungs-Fokus
|
||||||
const i = bausteine.value.findIndex((s) => s.title === baustein.title)
|
const i = bausteine.value.findIndex((s) => s.title === baustein.title)
|
||||||
@@ -286,6 +290,7 @@ function extractContext() {
|
|||||||
@close="fokusIndex = null"
|
@close="fokusIndex = null"
|
||||||
@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')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
Zum Thema "{topic}" haben {n} Agenten unabhängig über strittige Baustein-Einträge entschieden. Die Zahl in Klammern sagt, wie viele Agenten den Eintrag übernehmen wollen. Sortiere die Einträge.
|
|
||||||
|
|
||||||
BESCHLOSSENER KONSENS (nur Kontext — nicht ändern):
|
|
||||||
{konsens}
|
|
||||||
|
|
||||||
STRITTIGE EINTRÄGE MIT VOTUM:
|
|
||||||
{rest}
|
|
||||||
|
|
||||||
Regeln:
|
|
||||||
- Von der MEHRHEIT der {n} Agenten übernommen und keine Dublette zum Konsens → `aufnehmen`.
|
|
||||||
- Von NIEMANDEM übernommen oder Dublette zum Konsens → verwerfen (in keine Liste).
|
|
||||||
- Uneindeutig (Minderheits-Votum, fachlich unklar) → `rest`. Diese Einträge gehen in eine weitere Runde.{final}
|
|
||||||
- Übernimm Einträge wörtlich ("Titel — Kurzbeschreibung"), nicht umformulieren.
|
|
||||||
- Sind alle Einträge entschieden, ist `rest` LEER — das ist ein gültiges Ergebnis.
|
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
|
||||||
|
|
||||||
Format (kein weiterer Text in der Datei):
|
|
||||||
{{"aufnehmen": ["Titel — Kurzbeschreibung"], "rest": ["Titel — Kurzbeschreibung"]}}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
Bei der Auswahl der Bausteine zum Thema "{topic}" sind einige Einträge strittig. Entscheide für jeden strittigen Eintrag: aufnehmen oder verwerfen.
|
|
||||||
|
|
||||||
BESCHLOSSENER KONSENS (nur Kontext — nicht ändern):
|
|
||||||
{konsens}
|
|
||||||
|
|
||||||
STRITTIGE EINTRÄGE:
|
|
||||||
{rest}
|
|
||||||
|
|
||||||
Regeln:
|
|
||||||
- AUFNEHMEN, wenn der Eintrag eine eigenständige Lerneinheit des Themas ist, die der Konsens nicht abdeckt — auch nicht unter anderem Titel oder innerhalb eines Sammeleintrags.
|
|
||||||
- VERWERFEN, wenn er eine Dublette zum Konsens ist, nur eine Variante/Vertiefung eines vorhandenen Bausteins, Mikro-Granularität oder fachlich zweifelhaft.
|
|
||||||
- Übernimm aufgenommene Einträge wörtlich ("Titel — Kurzbeschreibung"), nicht umformulieren.
|
|
||||||
- Nichts Aufnehmenswertes dabei → leere Liste. Das ist ein gültiges Ergebnis.
|
|
||||||
|
|
||||||
Schreibe NUR die JSON-Datei nach: {out_path}
|
|
||||||
|
|
||||||
Format (kein weiterer Text in der Datei):
|
|
||||||
{{"uebernehmen": ["Titel — Kurzbeschreibung"]}}
|
|
||||||
Nichts aufnehmen: {{"uebernehmen": []}}
|
|
||||||
Reference in New Issue
Block a user