update
This commit is contained in:
@@ -22,7 +22,7 @@ from config import (
|
||||
)
|
||||
import lesbarkeit
|
||||
from database import list_guides, update_guide
|
||||
from fsutil import atomic_write_json
|
||||
from fsutil import atomic_write_json, atomic_write_text
|
||||
from jsonio import read_json_file as _json_datei
|
||||
from paths import bausteine_path, guide_content_path, project_dir, subbausteine_path
|
||||
from pipeline import (
|
||||
@@ -101,6 +101,35 @@ def guide_slot_dateien(content_path: Path) -> list[Path]:
|
||||
return [p for p in content_path.parent.glob(f"{content_path.stem}.*") if p != content_path]
|
||||
|
||||
|
||||
def _fertig_path(content_path: Path) -> Path:
|
||||
return content_path.parent / f"{content_path.stem}.fertig"
|
||||
|
||||
|
||||
def guide_fertig_step(content_path: Path) -> int:
|
||||
"""Höchster VOLL abgeschlossener Schritt-Index (Marker je Thema+Format). -1 = keiner.
|
||||
Existiert die Content-Datei, sind alle Schritte fertig."""
|
||||
if content_path.exists():
|
||||
return len(GUIDE_STEPS) - 1
|
||||
try:
|
||||
return int(_fertig_path(content_path).read_text(encoding="utf-8").strip())
|
||||
except (OSError, ValueError):
|
||||
return -1
|
||||
|
||||
|
||||
def _set_fertig(content_path: Path, step: int) -> None:
|
||||
"""Marker auf `step` setzen — monoton (nur erhöhen), außer beim Re-Run-Reset (force)."""
|
||||
if step > guide_fertig_step(content_path):
|
||||
atomic_write_text(_fertig_path(content_path), str(step))
|
||||
|
||||
|
||||
def _reset_fertig(content_path: Path, step: int) -> None:
|
||||
"""Marker hart auf `step` setzen (für Re-Run ab Schritt; step kann sinken)."""
|
||||
if step < 0:
|
||||
_fertig_path(content_path).unlink(missing_ok=True)
|
||||
else:
|
||||
atomic_write_text(_fertig_path(content_path), str(step))
|
||||
|
||||
|
||||
# Slot-Datei-Globs je Schritt (Index = GUIDE_STEPS). Stem-verankert, kollisionsfrei.
|
||||
_STEP_GLOBS = (
|
||||
("auswahl-*", "auswahl-mapping-*"), # 0 Auswahl (deterministisch → meist leer)
|
||||
@@ -121,6 +150,7 @@ def _reset_guide_ab_step(content_path: Path, step: int) -> None:
|
||||
for pat in globs:
|
||||
for p in d.glob(f"{stem}.{pat}"):
|
||||
p.unlink(missing_ok=True)
|
||||
_reset_fertig(content_path, step - 1) # Schritte < step gelten als fertig
|
||||
|
||||
|
||||
def _resolve_auswahl(data, entries: dict[int, str], k_min: int, k_max: int) -> list[int] | None:
|
||||
@@ -552,6 +582,7 @@ async def _generate_sections(
|
||||
|
||||
# Garantie: jeder gewählte Baustein steht im Plan (gegen weglassende Agenten/Judges).
|
||||
plan = _mit_resten(plan, sel_entries)
|
||||
_set_fertig(content_path, 1) # Gliederung steht
|
||||
|
||||
# Chunks festlegen: 1 Chunk = 1 Baustein. So läuft JEDER Section-Schritt (Inhalte,
|
||||
# Inhalts-Prüfung, Writer, Lese-Check) mit genau einem Agent je Baustein. Der Writer
|
||||
@@ -605,6 +636,8 @@ async def _generate_sections(
|
||||
if not inhalt_by_num:
|
||||
await _fail(guide_id, "Keine Inhalte identifiziert")
|
||||
return None
|
||||
if all(p.exists() for p in inhalt_paths):
|
||||
_set_fertig(content_path, 2) # Inhalte vollständig
|
||||
|
||||
inhalt_chunk_nums = [[num for ch in chunk for num in ch["nums"] if num in inhalt_by_num] for chunk in chunks]
|
||||
|
||||
@@ -670,6 +703,8 @@ async def _generate_sections(
|
||||
if num in probleme_by_num and sec["md"].strip():
|
||||
inhalt_by_num[num] = sec["md"]
|
||||
|
||||
_set_fertig(content_path, 3) # Inhalts-Check durch
|
||||
|
||||
# Schritt 4: Schreiben — Writer formuliert die geprüften Inhalte aus (Resume).
|
||||
def inhalte_text(chunk) -> str:
|
||||
nums = [num for ch in chunk for num in ch["nums"] if num in inhalt_by_num]
|
||||
@@ -718,6 +753,8 @@ async def _generate_sections(
|
||||
if not by_num:
|
||||
await _fail(guide_id, "Keine Sections in der Writer-Ausgabe gefunden")
|
||||
return None
|
||||
if all(p.exists() for p in paths):
|
||||
_set_fertig(content_path, 4) # Schreiben vollständig
|
||||
|
||||
# Schritt 3: Lese-Prüfungs-Loop — Check pro Writer-Paket, Fix nur für
|
||||
# beanstandete Sections; Folgerunden prüfen NUR die ersetzten Sections.
|
||||
@@ -821,6 +858,7 @@ async def _generate_sections(
|
||||
_log(topic, f"Lese-Prüfung: 1 Runde — Überarbeitung bleibt ungeprüft")
|
||||
break
|
||||
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
||||
_set_fertig(content_path, 5) # Lese-Prüfung durch
|
||||
|
||||
# Prüfbar = Format hat Prüfung UND Baustein hat ≥1 relevanten Subbaustein.
|
||||
# Guide ist immer prüfbar (auch ohne Relevanz-Daten, Fallback = alles).
|
||||
|
||||
@@ -19,7 +19,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 elements import generate_element, chat_with_guide, chat_with_element, check_element, style_element, refine_suggestion
|
||||
from lernen import NOETIG, MASTERY, MEISTERN, NIVEAUS, 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_meilensteinen, _zufall_malus
|
||||
from guide import generate_guide, guide_slot_dateien
|
||||
from guide import generate_guide, guide_slot_dateien, guide_fertig_step
|
||||
from pipeline import cancel_guide
|
||||
from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lernstand, thema_abgeschlossen
|
||||
from models import (
|
||||
@@ -474,6 +474,13 @@ async def guide_locks(topic: str):
|
||||
return {fmt: guide_lock(topic, fmt, guides, progress, levels) for fmt in ("FullGuide", "Rest", *FORMATE)}
|
||||
|
||||
|
||||
@router.get("/guides/steps")
|
||||
async def guide_steps(topic: str):
|
||||
"""Höchster voll abgeschlossener Schritt-Index je Format (artefakt-basiert, -1 = keiner).
|
||||
Treibt die klickbaren Schritt-Kugeln (wie die Bausteine-Phasen)."""
|
||||
return {fmt: guide_fertig_step(guide_content_path(topic, fmt)) for fmt in ("Guide", "FullGuide", "Rest")}
|
||||
|
||||
|
||||
@router.get("/guides/{guide_id}", response_model=GuideResponse)
|
||||
async def get_one(guide_id: str):
|
||||
guide = await get_guide(guide_id)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
|
||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProviders, fetchStats, fetchTopicFortschritt, fetchGuideLocks, fetchGuideSteps, fetchFolders, updateQuelle as apiUpdateQuelle } from './api.js'
|
||||
import { usePolling } from './composables/usePolling.js'
|
||||
import TopicSidebar from './components/TopicSidebar.vue'
|
||||
import TopicDetail from './components/TopicDetail.vue'
|
||||
@@ -30,6 +30,7 @@ const ansichtModus = ref('kompakt') // kompakt | erklärend — je
|
||||
const stats = ref(null)
|
||||
const fortschritt = ref({})
|
||||
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
||||
const guideStepsDone = ref({}) // höchster fertiger Schritt-Index je Format (artefakt-basiert)
|
||||
const uiError = ref(null) // abgewiesene Aktionen (409/400) sichtbar machen
|
||||
const elementsOpen = ref(false) // rechte Sidebar
|
||||
const elementsView = ref(false) // Übersicht im Hauptbereich
|
||||
@@ -170,10 +171,12 @@ async function loadBausteine() {
|
||||
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
||||
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
||||
locks.value = await fetchGuideLocks(selectedTopic.value)
|
||||
guideStepsDone.value = await fetchGuideSteps(selectedTopic.value)
|
||||
} else {
|
||||
bausteine.value = { ...EMPTY_BAUSTEINE }
|
||||
fortschritt.value = {}
|
||||
locks.value = {}
|
||||
guideStepsDone.value = {}
|
||||
}
|
||||
if (activeBausteine.value.length && !polling.running()) startPolling()
|
||||
} catch (e) {
|
||||
@@ -380,6 +383,7 @@ onMounted(async () => {
|
||||
:stats="stats"
|
||||
:fortschritt="fortschritt"
|
||||
:locks="locks"
|
||||
:guideStepsDone="guideStepsDone"
|
||||
:uiError="uiError"
|
||||
:doneByFormat="doneByFormat"
|
||||
:latestByFormat="latestByFormat"
|
||||
|
||||
@@ -18,6 +18,11 @@ export async function fetchGuides() {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchGuideSteps(topic) {
|
||||
const res = await fetch(`${BASE}/guides/steps?topic=${encodeURIComponent(topic)}`)
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function fetchGuideLocks(topic) {
|
||||
const res = await fetch(`${BASE}/guides/locks?topic=${encodeURIComponent(topic)}`)
|
||||
return res.json()
|
||||
|
||||
@@ -9,6 +9,7 @@ const props = defineProps({
|
||||
stats: { type: Object, default: null },
|
||||
fortschritt: { type: Object, default: () => ({}) },
|
||||
locks: { type: Object, default: () => ({}) },
|
||||
guideStepsDone: { type: Object, default: () => ({}) }, // höchster fertiger Schritt je Format (-1 = keiner)
|
||||
uiError: { type: String, default: null },
|
||||
doneByFormat: { type: Object, default: () => ({}) },
|
||||
latestByFormat: { type: Object, default: () => ({}) },
|
||||
@@ -121,35 +122,26 @@ function guideStatus(format) {
|
||||
// Schritt-Kugeln der Guide-Pipeline
|
||||
const GUIDE_STEPS = ['Auswahl', 'Gliederung', 'Inhalte', 'Inhalts-Check', 'Schreiben', 'Lese-Prüfung']
|
||||
|
||||
// Kugeln werden wie bei den Bausteinen immer angezeigt:
|
||||
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
|
||||
// Kugeln aus dem artefakt-basierten „fertig"-Marker (wie Bausteine, nicht aus dem DB-Zähler):
|
||||
// ≤ fertig = done. Läuft gerade → der nächste Schritt (fertig+1) ist aktiv.
|
||||
function guideSteps(format) {
|
||||
const labels = GUIDE_STEPS
|
||||
const fertig = props.guideStepsDone[format] ?? -1
|
||||
const st = guideStatus(format)
|
||||
if (st === 'generating' || st === 'queued') {
|
||||
// Clamp: alte DB-Läufe können step-Werte oberhalb der neuen Listen haben
|
||||
const step = Math.min(props.latestByFormat[format]?.step ?? -1, labels.length - 1)
|
||||
return labels.map((label, i) => ({
|
||||
label,
|
||||
state: i < step ? 'done' : i === step ? 'active' : 'pending',
|
||||
}))
|
||||
}
|
||||
if (props.doneByFormat[format]) {
|
||||
return labels.map((label) => ({ label, state: 'done' }))
|
||||
}
|
||||
if (abgebrochen(format)) {
|
||||
const step = Math.min(props.latestByFormat[format]?.step ?? 0, labels.length)
|
||||
return labels.map((label, i) => ({ label, state: i < step ? 'done' : 'pending' }))
|
||||
}
|
||||
return labels.map((label) => ({ label, state: 'pending' }))
|
||||
const aktiv = st === 'generating' || st === 'queued' ? fertig + 1 : -1
|
||||
return labels.map((label, i) => ({
|
||||
label,
|
||||
state: i <= fertig ? 'done' : i === aktiv ? 'active' : 'pending',
|
||||
}))
|
||||
}
|
||||
|
||||
// Re-Run ab Guide-Schritt (1-basierte Kugel je Format). null = voll/Resume.
|
||||
const gewaehlterStep = reactive({})
|
||||
// Kugeln klickbar, sobald der Guide (teil-)gebaut ist und gerade nicht generiert wird.
|
||||
// Kugeln klickbar, sobald Artefakte existieren (Marker ≥ 0 oder fertig) und nicht generiert wird.
|
||||
function guideWaehlbar(format) {
|
||||
const st = guideStatus(format)
|
||||
return (st === 'done' || abgebrochen(format)) && st !== 'generating' && st !== 'queued'
|
||||
if (st === 'generating' || st === 'queued') return false
|
||||
return (props.guideStepsDone[format] ?? -1) >= 0 || st === 'done'
|
||||
}
|
||||
function guideStepKlick(format, n) {
|
||||
if (!guideWaehlbar(format)) return
|
||||
|
||||
Reference in New Issue
Block a user