update
This commit is contained in:
@@ -22,7 +22,7 @@ from config import (
|
|||||||
)
|
)
|
||||||
import lesbarkeit
|
import lesbarkeit
|
||||||
from database import list_guides, update_guide
|
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 jsonio import read_json_file as _json_datei
|
||||||
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 (
|
||||||
@@ -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]
|
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.
|
# Slot-Datei-Globs je Schritt (Index = GUIDE_STEPS). Stem-verankert, kollisionsfrei.
|
||||||
_STEP_GLOBS = (
|
_STEP_GLOBS = (
|
||||||
("auswahl-*", "auswahl-mapping-*"), # 0 Auswahl (deterministisch → meist leer)
|
("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 pat in globs:
|
||||||
for p in d.glob(f"{stem}.{pat}"):
|
for p in d.glob(f"{stem}.{pat}"):
|
||||||
p.unlink(missing_ok=True)
|
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:
|
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).
|
# Garantie: jeder gewählte Baustein steht im Plan (gegen weglassende Agenten/Judges).
|
||||||
plan = _mit_resten(plan, sel_entries)
|
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,
|
# 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
|
# 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:
|
if not inhalt_by_num:
|
||||||
await _fail(guide_id, "Keine Inhalte identifiziert")
|
await _fail(guide_id, "Keine Inhalte identifiziert")
|
||||||
return None
|
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]
|
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():
|
if num in probleme_by_num and sec["md"].strip():
|
||||||
inhalt_by_num[num] = sec["md"]
|
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).
|
# Schritt 4: Schreiben — Writer formuliert die geprüften Inhalte aus (Resume).
|
||||||
def inhalte_text(chunk) -> str:
|
def inhalte_text(chunk) -> str:
|
||||||
nums = [num for ch in chunk for num in ch["nums"] if num in inhalt_by_num]
|
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:
|
if not by_num:
|
||||||
await _fail(guide_id, "Keine Sections in der Writer-Ausgabe gefunden")
|
await _fail(guide_id, "Keine Sections in der Writer-Ausgabe gefunden")
|
||||||
return None
|
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
|
# Schritt 3: Lese-Prüfungs-Loop — Check pro Writer-Paket, Fix nur für
|
||||||
# beanstandete Sections; Folgerunden prüfen NUR die ersetzten Sections.
|
# 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")
|
_log(topic, f"Lese-Prüfung: 1 Runde — Überarbeitung bleibt ungeprüft")
|
||||||
break
|
break
|
||||||
scope = [[num for num in nums if num in ersetzt] for nums in chunk_nums]
|
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.
|
# Prüfbar = Format hat Prüfung UND Baustein hat ≥1 relevanten Subbaustein.
|
||||||
# Guide ist immer prüfbar (auch ohne Relevanz-Daten, Fallback = alles).
|
# 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 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 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 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 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 (
|
||||||
@@ -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)}
|
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)
|
@router.get("/guides/{guide_id}", response_model=GuideResponse)
|
||||||
async def get_one(guide_id: str):
|
async def get_one(guide_id: str):
|
||||||
guide = await get_guide(guide_id)
|
guide = await get_guide(guide_id)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
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 { usePolling } from './composables/usePolling.js'
|
||||||
import TopicSidebar from './components/TopicSidebar.vue'
|
import TopicSidebar from './components/TopicSidebar.vue'
|
||||||
import TopicDetail from './components/TopicDetail.vue'
|
import TopicDetail from './components/TopicDetail.vue'
|
||||||
@@ -30,6 +30,7 @@ const ansichtModus = ref('kompakt') // kompakt | erklärend — je
|
|||||||
const stats = ref(null)
|
const stats = ref(null)
|
||||||
const fortschritt = ref({})
|
const fortschritt = ref({})
|
||||||
const locks = ref({}) // Sperr-Gründe pro Format (Backend = einzige Regel-Quelle)
|
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 uiError = ref(null) // abgewiesene Aktionen (409/400) sichtbar machen
|
||||||
const elementsOpen = ref(false) // rechte Sidebar
|
const elementsOpen = ref(false) // rechte Sidebar
|
||||||
const elementsView = ref(false) // Übersicht im Hauptbereich
|
const elementsView = ref(false) // Übersicht im Hauptbereich
|
||||||
@@ -170,10 +171,12 @@ async function loadBausteine() {
|
|||||||
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
bausteine.value = await fetchBausteineStatus(selectedTopic.value)
|
||||||
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
fortschritt.value = await fetchTopicFortschritt(selectedTopic.value)
|
||||||
locks.value = await fetchGuideLocks(selectedTopic.value)
|
locks.value = await fetchGuideLocks(selectedTopic.value)
|
||||||
|
guideStepsDone.value = await fetchGuideSteps(selectedTopic.value)
|
||||||
} else {
|
} else {
|
||||||
bausteine.value = { ...EMPTY_BAUSTEINE }
|
bausteine.value = { ...EMPTY_BAUSTEINE }
|
||||||
fortschritt.value = {}
|
fortschritt.value = {}
|
||||||
locks.value = {}
|
locks.value = {}
|
||||||
|
guideStepsDone.value = {}
|
||||||
}
|
}
|
||||||
if (activeBausteine.value.length && !polling.running()) startPolling()
|
if (activeBausteine.value.length && !polling.running()) startPolling()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -380,6 +383,7 @@ onMounted(async () => {
|
|||||||
:stats="stats"
|
:stats="stats"
|
||||||
:fortschritt="fortschritt"
|
:fortschritt="fortschritt"
|
||||||
:locks="locks"
|
:locks="locks"
|
||||||
|
:guideStepsDone="guideStepsDone"
|
||||||
:uiError="uiError"
|
:uiError="uiError"
|
||||||
:doneByFormat="doneByFormat"
|
:doneByFormat="doneByFormat"
|
||||||
:latestByFormat="latestByFormat"
|
:latestByFormat="latestByFormat"
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export async function fetchGuides() {
|
|||||||
return res.json()
|
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) {
|
export async function fetchGuideLocks(topic) {
|
||||||
const res = await fetch(`${BASE}/guides/locks?topic=${encodeURIComponent(topic)}`)
|
const res = await fetch(`${BASE}/guides/locks?topic=${encodeURIComponent(topic)}`)
|
||||||
return res.json()
|
return res.json()
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const props = defineProps({
|
|||||||
stats: { type: Object, default: null },
|
stats: { type: Object, default: null },
|
||||||
fortschritt: { type: Object, default: () => ({}) },
|
fortschritt: { type: Object, default: () => ({}) },
|
||||||
locks: { type: Object, default: () => ({}) },
|
locks: { type: Object, default: () => ({}) },
|
||||||
|
guideStepsDone: { type: Object, default: () => ({}) }, // höchster fertiger Schritt je Format (-1 = keiner)
|
||||||
uiError: { type: String, default: null },
|
uiError: { type: String, default: null },
|
||||||
doneByFormat: { type: Object, default: () => ({}) },
|
doneByFormat: { type: Object, default: () => ({}) },
|
||||||
latestByFormat: { type: Object, default: () => ({}) },
|
latestByFormat: { type: Object, default: () => ({}) },
|
||||||
@@ -121,35 +122,26 @@ function guideStatus(format) {
|
|||||||
// Schritt-Kugeln der Guide-Pipeline
|
// Schritt-Kugeln der Guide-Pipeline
|
||||||
const GUIDE_STEPS = ['Auswahl', 'Gliederung', 'Inhalte', 'Inhalts-Check', 'Schreiben', 'Lese-Prüfung']
|
const GUIDE_STEPS = ['Auswahl', 'Gliederung', 'Inhalte', 'Inhalts-Check', 'Schreiben', 'Lese-Prüfung']
|
||||||
|
|
||||||
// Kugeln werden wie bei den Bausteinen immer angezeigt:
|
// Kugeln aus dem artefakt-basierten „fertig"-Marker (wie Bausteine, nicht aus dem DB-Zähler):
|
||||||
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
|
// ≤ fertig = done. Läuft gerade → der nächste Schritt (fertig+1) ist aktiv.
|
||||||
function guideSteps(format) {
|
function guideSteps(format) {
|
||||||
const labels = GUIDE_STEPS
|
const labels = GUIDE_STEPS
|
||||||
|
const fertig = props.guideStepsDone[format] ?? -1
|
||||||
const st = guideStatus(format)
|
const st = guideStatus(format)
|
||||||
if (st === 'generating' || st === 'queued') {
|
const aktiv = st === 'generating' || st === 'queued' ? fertig + 1 : -1
|
||||||
// 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) => ({
|
return labels.map((label, i) => ({
|
||||||
label,
|
label,
|
||||||
state: i < step ? 'done' : i === step ? 'active' : 'pending',
|
state: i <= fertig ? 'done' : i === aktiv ? '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' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-Run ab Guide-Schritt (1-basierte Kugel je Format). null = voll/Resume.
|
// Re-Run ab Guide-Schritt (1-basierte Kugel je Format). null = voll/Resume.
|
||||||
const gewaehlterStep = reactive({})
|
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) {
|
function guideWaehlbar(format) {
|
||||||
const st = guideStatus(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) {
|
function guideStepKlick(format, n) {
|
||||||
if (!guideWaehlbar(format)) return
|
if (!guideWaehlbar(format)) return
|
||||||
|
|||||||
Reference in New Issue
Block a user