update
This commit is contained in:
@@ -356,6 +356,17 @@ def _resume_step(topic: str) -> int:
|
|||||||
return _step_idx(topic, "Stufen finden")
|
return _step_idx(topic, "Stufen finden")
|
||||||
|
|
||||||
|
|
||||||
|
def _feine_status(topic: str, current: int | None) -> list[dict]:
|
||||||
|
"""Feiner Teilschritt-Status: je Schritt {label, phase, state}. state aus `current`
|
||||||
|
(done = idx<current, active = ==, pending = >). Phasen-Label aus PHASEN."""
|
||||||
|
schritt_phase = {s: label for label, schritte in PHASEN for s in schritte}
|
||||||
|
out = []
|
||||||
|
for i, s in enumerate(_bausteine_steps(topic)):
|
||||||
|
state = "pending" if current is None or current < i else "done" if current > i else "active"
|
||||||
|
out.append({"label": s, "phase": schritt_phase.get(s, ""), "state": state})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def bausteine_status(topic: str) -> dict:
|
def bausteine_status(topic: str) -> dict:
|
||||||
# Intern feingranular (Resume/Progress); für die Anzeige zu 5 groben Phasen gebündelt.
|
# Intern feingranular (Resume/Progress); für die Anzeige zu 5 groben Phasen gebündelt.
|
||||||
feine = _bausteine_steps(topic)
|
feine = _bausteine_steps(topic)
|
||||||
@@ -376,6 +387,7 @@ def bausteine_status(topic: str) -> dict:
|
|||||||
"error": _bausteine_errors.get(topic),
|
"error": _bausteine_errors.get(topic),
|
||||||
"partial": partial,
|
"partial": partial,
|
||||||
"steps": _phasen_status(topic, current),
|
"steps": _phasen_status(topic, current),
|
||||||
|
"feine_steps": _feine_status(topic, current),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -450,6 +462,91 @@ def _reset_ab_phase(topic: str, label: str) -> None:
|
|||||||
files["final"].unlink(missing_ok=True)
|
files["final"].unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
async def _reset_ab_step(topic: str, step_idx: int) -> None:
|
||||||
|
"""Feiner Reset AB einem Teilschritt (0-basierter Index in _bausteine_steps). Setzt
|
||||||
|
pipeline_state + Artefakte + DB ab hier zurück; frühere Schritte bleiben. Inventar-Teilschritte
|
||||||
|
rekonstruieren den DB-Status aus den Artefakten (Dedup/Filter sicher; Klärung fällt robust auf
|
||||||
|
Konsolidierung zurück, weil die Klärung umbenennt → Titel-Mismatch wäre fragil)."""
|
||||||
|
feine = list(_bausteine_steps(topic))
|
||||||
|
if not (0 <= step_idx < len(feine)):
|
||||||
|
return
|
||||||
|
ab = set(feine[step_idx:])
|
||||||
|
files = _bausteine_files(topic)
|
||||||
|
arbeit = files["arbeit"]
|
||||||
|
|
||||||
|
def gd(pat: str) -> None:
|
||||||
|
if arbeit.is_dir():
|
||||||
|
for p in arbeit.glob(pat):
|
||||||
|
p.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
await db.delete_pipeline_state(topic, list(feine[step_idx:]))
|
||||||
|
# Spätere Artefakte/DB kumulativ ab dem betroffenen Schritt (von hinten nach vorn).
|
||||||
|
if {"Beispiele", "Karteikarten"} & ab:
|
||||||
|
files["artefakte"].unlink(missing_ok=True); gd("artefakt-*"); await db.delete_sub_artefakte(topic)
|
||||||
|
if any(s.startswith("Fragen") for s in ab):
|
||||||
|
files["frage_muster"].unlink(missing_ok=True); gd("frage-muster-*"); await db.delete_frage_muster(topic)
|
||||||
|
if "Gliederung" in ab:
|
||||||
|
files["gliederung"].unlink(missing_ok=True); gd("gliederung-*"); await db.delete_gliederung(topic)
|
||||||
|
if any(s.startswith("Relevanz") for s in ab):
|
||||||
|
gd("relevanz-*")
|
||||||
|
if any(s.startswith("Stufen") for s in ab):
|
||||||
|
gd("stufe-*")
|
||||||
|
if any(s.startswith("Fakten") for s in ab):
|
||||||
|
files["fakten"].unlink(missing_ok=True); gd("fakten-*")
|
||||||
|
# Sidecar trägt Subbausteine + ihre Felder stufe/relevanz/fakten. Ab Subbausteinen ganz neu;
|
||||||
|
# sonst nur die Felder der neu-zu-bauenden Phasen strippen — Subbausteine bleiben erhalten.
|
||||||
|
if any(s.startswith("Subbaustein") for s in ab):
|
||||||
|
files["sidecar"].unlink(missing_ok=True)
|
||||||
|
else:
|
||||||
|
strip = {f for s, f in (("Fakten", "fakten"), ("Stufen", "stufe"), ("Relevanz", "relevanz"))
|
||||||
|
if any(x.startswith(s) for x in ab)}
|
||||||
|
if strip:
|
||||||
|
sc = _json_datei(files["sidecar"])
|
||||||
|
if isinstance(sc, dict):
|
||||||
|
for subs in sc.values():
|
||||||
|
for s in (subs if isinstance(subs, list) else []):
|
||||||
|
if isinstance(s, dict):
|
||||||
|
for f in strip:
|
||||||
|
s.pop(f, None)
|
||||||
|
atomic_write_json(files["sidecar"], sc, indent=1)
|
||||||
|
if any(s.startswith("Subbaustein") for s in ab):
|
||||||
|
files["sub_roh"].unlink(missing_ok=True); gd("subbaustein-*"); await db.delete_subbausteine(topic)
|
||||||
|
# --- Inventar (DB-Status kaskadiert) ---
|
||||||
|
if "Bausteine-Filter" in ab and not ({"Klärung", "Konsolidierung", "Recherche", "Dedup"} & ab):
|
||||||
|
# Nur Filter neu: degradierte Bausteine zurück auf konsens.
|
||||||
|
d = _json_datei(arbeit / "inventar-filter.json")
|
||||||
|
for f in (d.get("fragmente", []) if isinstance(d, dict) else []):
|
||||||
|
await db.set_baustein_status(topic, _norm_titel(f.get("fragment", "")), "konsens")
|
||||||
|
gd("inventar-filter*")
|
||||||
|
if "Dedup" in ab and not ({"Klärung", "Konsolidierung", "Recherche"} & ab):
|
||||||
|
# Dedup (+Filter) neu: alle im Dedup/Filter verworfenen Bausteine zurück auf konsens.
|
||||||
|
for art in ("dedup-runde-1.json", "inventar-filter.json"):
|
||||||
|
d = _json_datei(arbeit / art)
|
||||||
|
titel = ([t for g in d.get("gruppen", []) for t in g] if isinstance(d, dict) and "gruppen" in d
|
||||||
|
else [f.get("fragment", "") for f in d.get("fragmente", [])] if isinstance(d, dict) else [])
|
||||||
|
for t in titel:
|
||||||
|
await db.set_baustein_status(topic, _norm_titel(t), "konsens")
|
||||||
|
gd("dedup-*"); gd("inventar-filter*")
|
||||||
|
if {"Klärung", "Konsolidierung"} & ab and not ({"Recherche"} & ab):
|
||||||
|
# Klärung/Konsolidierung neu: Inventar-DB leeren (Recherche-Reader bleiben). Klärungs-Rückbau
|
||||||
|
# wäre wegen Umbenennung fragil → ab Konsolidierung sauber neu aufbauen.
|
||||||
|
await db.delete_bausteine(topic)
|
||||||
|
gd("klaerung*"); gd("konsolidierung-*"); gd("dedup-*"); gd("inventar-filter*")
|
||||||
|
if "Recherche" in ab: # ganzes Inventar wie Phase-Reset
|
||||||
|
for p_alt in _alle_slot_dateien(files):
|
||||||
|
p_alt.unlink(missing_ok=True)
|
||||||
|
files["final"].unlink(missing_ok=True)
|
||||||
|
await db.delete_bausteine(topic)
|
||||||
|
|
||||||
|
|
||||||
|
async def reset_bausteine_ab_step(topic: str, step_idx: int) -> None:
|
||||||
|
"""Öffentlich: NUR ab einem Teilschritt zurücksetzen — kein Neu-Generieren. Lässt einen
|
||||||
|
Teil-Stand zurück (die Schritte ab hier gelten als offen). Läuft eine Generierung → ignorieren."""
|
||||||
|
if topic in _bausteine_progress:
|
||||||
|
return
|
||||||
|
await _reset_ab_step(topic, step_idx)
|
||||||
|
|
||||||
|
|
||||||
def _ergaenzung_schema(data):
|
def _ergaenzung_schema(data):
|
||||||
"""{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None."""
|
"""{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None."""
|
||||||
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
||||||
@@ -2999,7 +3096,7 @@ async def _reset_db_ab_phase(topic: str, label: str) -> None:
|
|||||||
await db.delete_pipeline_state(topic, ["Quelle aufbereiten"])
|
await db.delete_pipeline_state(topic, ["Quelle aufbereiten"])
|
||||||
|
|
||||||
|
|
||||||
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None) -> None:
|
async def generate_bausteine(topic: str, instructions: str = "", provider: str = DEFAULT_PROVIDER, ab_phase: int | None = None, ab_step: int | None = None) -> None:
|
||||||
if topic in _bausteine_progress:
|
if topic in _bausteine_progress:
|
||||||
return
|
return
|
||||||
_bausteine_progress[topic] = "Wartend…"
|
_bausteine_progress[topic] = "Wartend…"
|
||||||
@@ -3029,7 +3126,9 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
||||||
# Re-Run ab gewählter Phase: Artefakte ab dort löschen; der Frischstart-Block
|
# Re-Run ab gewählter Phase: Artefakte ab dort löschen; der Frischstart-Block
|
||||||
# unten wird übersprungen (er würde bei erhaltener Sidecar sonst alles wischen).
|
# unten wird übersprungen (er würde bei erhaltener Sidecar sonst alles wischen).
|
||||||
if ab_phase is not None:
|
if ab_step is not None: # feiner Teilschritt-Re-Run (Vorrang vor ab_phase)
|
||||||
|
await _reset_ab_step(topic, ab_step)
|
||||||
|
elif ab_phase is not None:
|
||||||
phasen = _phasen(topic)
|
phasen = _phasen(topic)
|
||||||
label = phasen[ab_phase - 1][0] if 1 <= ab_phase <= len(phasen) else "Inventar"
|
label = phasen[ab_phase - 1][0] if 1 <= ab_phase <= len(phasen) else "Inventar"
|
||||||
_reset_ab_phase(topic, label)
|
_reset_ab_phase(topic, label)
|
||||||
@@ -3043,7 +3142,7 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
# Sidecar) → kompletter Frischstart. Liegt bausteine.md ohne Sidecar vor,
|
||||||
# ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen.
|
# ist das ein Teil-Stand (Block B/C offen) → Resume, nicht wischen.
|
||||||
# Bei explizitem Re-Run (ab_phase) hat _reset_ab_phase das schon erledigt.
|
# Bei explizitem Re-Run (ab_phase) hat _reset_ab_phase das schon erledigt.
|
||||||
fertig = ab_phase is None and final_path.exists() and _sidecar_schema(_json_datei(files["sidecar"])) is not None
|
fertig = ab_phase is None and ab_step is None and final_path.exists() and _sidecar_schema(_json_datei(files["sidecar"])) is not None
|
||||||
if fertig:
|
if fertig:
|
||||||
for p_alt in _alle_slot_dateien(files):
|
for p_alt in _alle_slot_dateien(files):
|
||||||
p_alt.unlink(missing_ok=True)
|
p_alt.unlink(missing_ok=True)
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ class BausteineCreateRequest(BaseModel):
|
|||||||
source_type: SourceType = "thema"
|
source_type: SourceType = "thema"
|
||||||
source_ort: str = Field(default="", max_length=2000)
|
source_ort: str = Field(default="", max_length=2000)
|
||||||
ab_phase: int | None = Field(default=None, ge=1, le=9) # Re-Run ab grober Phase (Position in _phasen(topic), 1-based; bis 9: …Gliederung/Fragen/Artefakte); None = Resume/Fortsetzen ohne Löschen
|
ab_phase: int | None = Field(default=None, ge=1, le=9) # Re-Run ab grober Phase (Position in _phasen(topic), 1-based; bis 9: …Gliederung/Fragen/Artefakte); None = Resume/Fortsetzen ohne Löschen
|
||||||
|
ab_step: int | None = Field(default=None, ge=0) # Re-Run ab feinem Teilschritt (0-basierter Index in _bausteine_steps); hat Vorrang vor ab_phase
|
||||||
|
|
||||||
|
|
||||||
|
class BausteineResetStepRequest(BaseModel):
|
||||||
|
topic: str = Field(min_length=1, max_length=100)
|
||||||
|
ab_step: int = Field(ge=0) # NUR zurücksetzen ab diesem Teilschritt (kein Neu-Generieren)
|
||||||
|
|
||||||
|
|
||||||
class BausteineStep(BaseModel):
|
class BausteineStep(BaseModel):
|
||||||
@@ -38,6 +44,12 @@ class BausteineStep(BaseModel):
|
|||||||
state: Literal["done", "active", "pending"]
|
state: Literal["done", "active", "pending"]
|
||||||
|
|
||||||
|
|
||||||
|
class BausteineFeinStep(BaseModel):
|
||||||
|
label: str
|
||||||
|
phase: str = ""
|
||||||
|
state: Literal["done", "active", "pending"]
|
||||||
|
|
||||||
|
|
||||||
class BausteineStatusResponse(BaseModel):
|
class BausteineStatusResponse(BaseModel):
|
||||||
ready: bool
|
ready: bool
|
||||||
generating: bool
|
generating: bool
|
||||||
@@ -45,6 +57,7 @@ class BausteineStatusResponse(BaseModel):
|
|||||||
error: str | None = None
|
error: str | None = None
|
||||||
partial: bool = False
|
partial: bool = False
|
||||||
steps: list[BausteineStep] = []
|
steps: list[BausteineStep] = []
|
||||||
|
feine_steps: list[BausteineFeinStep] = []
|
||||||
|
|
||||||
|
|
||||||
class ProjectResponse(BaseModel):
|
class ProjectResponse(BaseModel):
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from database import (
|
|||||||
delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content,
|
delete_topic_pipeline, delete_quelle, get_guide_content, delete_guide_content,
|
||||||
get_sub_artefakte,
|
get_sub_artefakte,
|
||||||
)
|
)
|
||||||
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, lade_quelle, lade_uebersicht, subbausteine_titel, subbausteine_frei, lade_frage_muster, lade_frage_muster_frei
|
from bausteine import generate_bausteine, cancel_bausteine, bausteine_status, active_bausteine, reset_bausteine, reset_bausteine_ab_step, lade_quelle, lade_uebersicht, subbausteine_titel, subbausteine_frei, lade_frage_muster, lade_frage_muster_frei
|
||||||
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 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, huerden_distraktor_block, score_berechnen, floor_aus_score, stufe_aus_score, cap_final, cap_aktuell, freie_ebene, schwellen, punkte_delta, deckel_nachfrage
|
from lernen import 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, huerden_distraktor_block, score_berechnen, floor_aus_score, stufe_aus_score, cap_final, cap_aktuell, freie_ebene, schwellen, punkte_delta, deckel_nachfrage
|
||||||
from guide import generate_guide, guide_slot_dateien, guide_fertig_step, block_pruefen, block_uebernehmen, content_fuer_ebene
|
from guide import generate_guide, guide_slot_dateien, guide_fertig_step, block_pruefen, block_uebernehmen, content_fuer_ebene
|
||||||
@@ -29,7 +29,7 @@ from regeln import FORMATE, formate_stats, guide_lock, ist_absolviert, lade_lern
|
|||||||
from models import (
|
from models import (
|
||||||
GuideCreateRequest, GuideResponse,
|
GuideCreateRequest, GuideResponse,
|
||||||
TopicCreateRequest,
|
TopicCreateRequest,
|
||||||
BausteineCreateRequest, BausteineStatusResponse,
|
BausteineCreateRequest, BausteineResetStepRequest, BausteineStatusResponse,
|
||||||
GuideChatRequest, GuideChatResponse,
|
GuideChatRequest, GuideChatResponse,
|
||||||
ElementCreateRequest, ElementChatRequest, ElementChatResponse, ElementResponse,
|
ElementCreateRequest, ElementChatRequest, ElementChatResponse, ElementResponse,
|
||||||
ElementUpdateRequest, ElementCheckRequest, ElementCheckResponse, ElementStyleResponse,
|
ElementUpdateRequest, ElementCheckRequest, ElementCheckResponse, ElementStyleResponse,
|
||||||
@@ -164,7 +164,7 @@ async def create_bausteine(req: BausteineCreateRequest):
|
|||||||
raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.")
|
raise HTTPException(400, "Link muss mit http:// oder https:// beginnen.")
|
||||||
qp.parent.mkdir(parents=True, exist_ok=True)
|
qp.parent.mkdir(parents=True, exist_ok=True)
|
||||||
atomic_write_json(qp, {"type": typ, "ort": ort, "spec": req.instructions.strip()})
|
atomic_write_json(qp, {"type": typ, "ort": ort, "spec": req.instructions.strip()})
|
||||||
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase))
|
asyncio.create_task(generate_bausteine(topic, req.instructions.strip(), req.provider, ab_phase=req.ab_phase, ab_step=req.ab_step))
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -182,6 +182,15 @@ async def remove_bausteine(topic: str):
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/bausteine/reset-step")
|
||||||
|
async def reset_bausteine_step(req: BausteineResetStepRequest):
|
||||||
|
topic = req.topic.strip()
|
||||||
|
if bausteine_status(topic)["generating"]:
|
||||||
|
return {"ok": True, "status": "generating"} # nicht in laufende Generierung eingreifen
|
||||||
|
await reset_bausteine_ab_step(topic, req.ab_step)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/bausteine/fortschritt")
|
@router.delete("/bausteine/fortschritt")
|
||||||
async def reset_baustein_fortschritt(topic: str, baustein: str):
|
async def reset_baustein_fortschritt(topic: str, baustein: str):
|
||||||
"""Lern-Fortschritt EINES Bausteins auf null (Score/Streak/Flags/offene Frage)."""
|
"""Lern-Fortschritt EINES Bausteins auf null (Score/Streak/Flags/offene Frage)."""
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch, onMounted } from 'vue'
|
import { ref, computed, watch, onMounted } 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, fetchGuideSteps, 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, resetBausteineAbStep as apiResetBausteineAbStep, 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'
|
||||||
@@ -21,7 +21,7 @@ const darkMode = ref(
|
|||||||
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
? window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
: localStorage.getItem('darkMode') === 'true',
|
: localStorage.getItem('darkMode') === 'true',
|
||||||
)
|
)
|
||||||
const EMPTY_BAUSTEINE = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [] }
|
const EMPTY_BAUSTEINE = { ready: false, generating: false, progress: null, error: null, partial: false, steps: [], feine_steps: [] }
|
||||||
const bausteine = ref({ ...EMPTY_BAUSTEINE })
|
const bausteine = ref({ ...EMPTY_BAUSTEINE })
|
||||||
const activeBausteine = ref([])
|
const activeBausteine = ref([])
|
||||||
const provider = ref(localStorage.getItem('provider') || 'claude')
|
const provider = ref(localStorage.getItem('provider') || 'claude')
|
||||||
@@ -227,12 +227,24 @@ async function handleResetBausteine() {
|
|||||||
await loadBausteine()
|
await loadBausteine()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleBausteineClick({ instructions, abPhase = null }) {
|
async function handleResetAbStep(step) {
|
||||||
if (!selectedTopic.value) return
|
if (!selectedTopic.value) return
|
||||||
uiError.value = null
|
uiError.value = null
|
||||||
try {
|
try {
|
||||||
// Quelle ist hier schon festgeschrieben; nur abPhase steuert den Re-Run-Umfang.
|
await apiResetBausteineAbStep(selectedTopic.value, step) // nur zurücksetzen, kein Neu-Generieren
|
||||||
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase)
|
} catch (e) {
|
||||||
|
uiError.value = e.message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await loadBausteine()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleBausteineClick({ instructions, abPhase = null, abStep = null }) {
|
||||||
|
if (!selectedTopic.value) return
|
||||||
|
uiError.value = null
|
||||||
|
try {
|
||||||
|
// Quelle ist hier schon festgeschrieben; abPhase/abStep steuern den Re-Run-Umfang.
|
||||||
|
await apiCreateBausteine(selectedTopic.value, instructions, provider.value, undefined, undefined, abPhase, abStep)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
uiError.value = e.message
|
uiError.value = e.message
|
||||||
return
|
return
|
||||||
@@ -428,7 +440,17 @@ onMounted(async () => {
|
|||||||
<BausteineUebersicht
|
<BausteineUebersicht
|
||||||
v-if="selectedTopic && bausteineView"
|
v-if="selectedTopic && bausteineView"
|
||||||
:topic="selectedTopic"
|
:topic="selectedTopic"
|
||||||
|
:steps="bausteine.feine_steps || []"
|
||||||
|
:generating="bausteine.generating"
|
||||||
|
:progress="bausteine.progress"
|
||||||
|
:ready="bausteine.ready"
|
||||||
|
:partial="bausteine.partial"
|
||||||
@close="bausteineView = false"
|
@close="bausteineView = false"
|
||||||
|
@restartFrom="(i) => handleBausteineClick({ instructions: '', abStep: i })"
|
||||||
|
@resetFrom="handleResetAbStep"
|
||||||
|
@restartAll="() => handleBausteineClick({ abPhase: bausteine.ready ? 1 : null })"
|
||||||
|
@removeAll="handleResetBausteine"
|
||||||
|
@cancel="handleCancelBausteine"
|
||||||
/>
|
/>
|
||||||
<ElementsOverview
|
<ElementsOverview
|
||||||
v-else-if="selectedTopic && elementsView"
|
v-else-if="selectedTopic && elementsView"
|
||||||
|
|||||||
@@ -47,11 +47,20 @@ export async function fetchBausteineStatus(topic) {
|
|||||||
return res.json()
|
return res.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null) {
|
export async function createBausteine(topic, instructions = '', provider = 'claude', sourceType = 'thema', sourceOrt = '', abPhase = null, abStep = null) {
|
||||||
const res = await fetch(`${BASE}/bausteine`, {
|
const res = await fetch(`${BASE}/bausteine`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt, ab_phase: abPhase }),
|
body: JSON.stringify({ topic, instructions, provider, source_type: sourceType, source_ort: sourceOrt, ab_phase: abPhase, ab_step: abStep }),
|
||||||
|
})
|
||||||
|
return jsonOrThrow(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resetBausteineAbStep(topic, abStep) {
|
||||||
|
const res = await fetch(`${BASE}/bausteine/reset-step`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ topic, ab_step: abStep }),
|
||||||
})
|
})
|
||||||
return jsonOrThrow(res)
|
return jsonOrThrow(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,41 @@ import { fetchBausteineUebersicht } from '../api.js'
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
topic: { type: String, required: true },
|
topic: { type: String, required: true },
|
||||||
|
steps: { type: Array, default: () => [] }, // feine Teilschritte {label, phase, state}
|
||||||
|
generating: { type: Boolean, default: false },
|
||||||
|
progress: { type: String, default: null },
|
||||||
|
ready: { type: Boolean, default: false },
|
||||||
|
partial: { type: Boolean, default: false },
|
||||||
})
|
})
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close', 'restartFrom', 'resetFrom', 'restartAll', 'removeAll', 'cancel'])
|
||||||
|
|
||||||
|
// Teilschritte nach Phase gruppieren, globalen Index für den Re-Run mitführen.
|
||||||
|
const phasenGruppen = computed(() => {
|
||||||
|
const out = []
|
||||||
|
props.steps.forEach((s, i) => {
|
||||||
|
const last = out[out.length - 1]
|
||||||
|
if (last && last.phase === s.phase) last.schritte.push({ ...s, idx: i })
|
||||||
|
else out.push({ phase: s.phase, schritte: [{ ...s, idx: i }] })
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
})
|
||||||
|
|
||||||
|
const gewaehlt = ref(null) // markierter Startpunkt (Schritt-Index)
|
||||||
|
const confirm = ref(null) // welche destruktive Aktion gerade „Sicher?" zeigt
|
||||||
|
const gewaehltLabel = computed(() => props.steps[gewaehlt.value]?.label || '')
|
||||||
|
|
||||||
|
function stepKlick(idx) {
|
||||||
|
if (props.generating) return
|
||||||
|
gewaehlt.value = gewaehlt.value === idx ? null : idx
|
||||||
|
confirm.value = null
|
||||||
|
}
|
||||||
|
// 2-Klick-Bestätigung für destruktive Aktionen: erster Klick „armt", zweiter führt aus.
|
||||||
|
function arm(aktion, fn) {
|
||||||
|
if (confirm.value === aktion) { confirm.value = null; fn() }
|
||||||
|
else confirm.value = aktion
|
||||||
|
}
|
||||||
|
function neuAbHier() { const i = gewaehlt.value; gewaehlt.value = null; confirm.value = null; emit('restartFrom', i) }
|
||||||
|
function loeschAbHier() { const i = gewaehlt.value; gewaehlt.value = null; confirm.value = null; emit('resetFrom', i) }
|
||||||
|
|
||||||
const items = ref([])
|
const items = ref([])
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
@@ -62,6 +95,46 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
|||||||
<button class="bk-close" title="Schließen" @click="emit('close')">✕</button>
|
<button class="bk-close" title="Schließen" @click="emit('close')">✕</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section v-if="steps.length" class="bk-steps">
|
||||||
|
<div class="bk-steps-top">
|
||||||
|
<div v-if="progress" class="bk-progress"><span class="bk-progress-dot"></span>{{ progress }}</div>
|
||||||
|
<div v-if="!generating" class="bk-global-actions">
|
||||||
|
<button class="bk-act play" @click="emit('restartAll')">{{ partial ? 'Fortsetzen' : ready ? 'Neu generieren' : 'Generieren' }}</button>
|
||||||
|
<button
|
||||||
|
v-if="ready || partial"
|
||||||
|
class="bk-act danger"
|
||||||
|
:class="{ armed: confirm === 'remove' }"
|
||||||
|
@click="arm('remove', () => emit('removeAll'))"
|
||||||
|
>{{ confirm === 'remove' ? 'Sicher?' : 'Entfernen' }}</button>
|
||||||
|
</div>
|
||||||
|
<div v-else class="bk-global-actions">
|
||||||
|
<button class="bk-act danger" @click="emit('cancel')">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bk-phasen">
|
||||||
|
<div v-for="g in phasenGruppen" :key="g.phase" class="bk-phase">
|
||||||
|
<span class="bk-phase-label">{{ g.phase }}</span>
|
||||||
|
<div class="bk-schritte">
|
||||||
|
<button
|
||||||
|
v-for="s in g.schritte"
|
||||||
|
:key="s.idx"
|
||||||
|
class="bk-step"
|
||||||
|
:class="[s.state, { sel: gewaehlt === s.idx }]"
|
||||||
|
:disabled="generating"
|
||||||
|
:title="`Startpunkt «${s.label}» wählen`"
|
||||||
|
@click="stepKlick(s.idx)"
|
||||||
|
>{{ s.label }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="gewaehlt !== null && !generating" class="bk-step-actions">
|
||||||
|
<span class="bk-step-actions-label">Ab «{{ gewaehltLabel }}»:</span>
|
||||||
|
<button class="bk-act play" @click="neuAbHier">↻ neu generieren</button>
|
||||||
|
<button class="bk-act danger" :class="{ armed: confirm === 'reset' }" @click="arm('reset', loeschAbHier)">{{ confirm === 'reset' ? 'Sicher?' : '✕ alles löschen' }}</button>
|
||||||
|
<button class="bk-act ghost" @click="gewaehlt = null; confirm = null">Abbrechen</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div v-if="loading" class="bk-empty-state">Lade…</div>
|
<div v-if="loading" class="bk-empty-state">Lade…</div>
|
||||||
<div v-else-if="error" class="bk-empty-state">{{ error }}</div>
|
<div v-else-if="error" class="bk-empty-state">{{ error }}</div>
|
||||||
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
|
<div v-else-if="!items.length" class="bk-empty-state">Noch keine Bausteine.</div>
|
||||||
@@ -126,6 +199,93 @@ const subTotal = computed(() => items.value.reduce((n, b) => n + (b.subbausteine
|
|||||||
}
|
}
|
||||||
.bk-close:hover { border-color: var(--accent); }
|
.bk-close:hover { border-color: var(--accent); }
|
||||||
|
|
||||||
|
/* Schritt-Übersicht über den Bausteinen */
|
||||||
|
.bk-steps {
|
||||||
|
padding: 0.85rem 2rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--panel-soft);
|
||||||
|
}
|
||||||
|
.bk-progress {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.7rem;
|
||||||
|
}
|
||||||
|
.bk-progress-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
animation: bk-pulse 1.2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes bk-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||||
|
|
||||||
|
.bk-phasen { display: flex; flex-wrap: wrap; gap: 0.5rem 1.1rem; }
|
||||||
|
.bk-phase { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||||
|
.bk-phase-label {
|
||||||
|
font-size: 0.62rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
.bk-schritte { display: flex; flex-wrap: wrap; gap: 0.3rem; }
|
||||||
|
.bk-step {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
padding: 0.22rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.bk-step:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
|
||||||
|
.bk-step:disabled { cursor: default; opacity: 0.7; }
|
||||||
|
.bk-step.done { border-color: var(--success-border); color: var(--success); }
|
||||||
|
.bk-step.active { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); font-weight: 600; }
|
||||||
|
.bk-step.pending { color: var(--text-faint); }
|
||||||
|
.bk-step.sel { border-color: var(--accent); color: var(--on-accent); background: var(--accent); font-weight: 700; box-shadow: 0 0 0 2px var(--accent-soft); }
|
||||||
|
|
||||||
|
/* Kopf: Fortschritt links, globale Buttons rechts */
|
||||||
|
.bk-steps-top { display: flex; align-items: center; gap: 1rem; min-height: 1.9rem; margin-bottom: 0.7rem; }
|
||||||
|
.bk-steps-top .bk-progress { margin-bottom: 0; }
|
||||||
|
.bk-global-actions { margin-left: auto; display: flex; gap: 0.4rem; }
|
||||||
|
|
||||||
|
/* Aktions-Leiste bei gewähltem Startpunkt */
|
||||||
|
.bk-step-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding-top: 0.7rem;
|
||||||
|
border-top: 1px dashed var(--border-strong);
|
||||||
|
}
|
||||||
|
.bk-step-actions-label { font-size: 0.8rem; font-weight: 600; color: var(--text-muted); }
|
||||||
|
|
||||||
|
.bk-act {
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--panel);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 0.3rem 0.7rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.bk-act:hover { border-color: var(--accent); }
|
||||||
|
.bk-act.play { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
|
||||||
|
.bk-act.play:hover { background: var(--accent-hover); }
|
||||||
|
.bk-act.danger { color: var(--danger); border-color: var(--danger); background: transparent; }
|
||||||
|
.bk-act.danger.armed { background: var(--danger); color: #fff; }
|
||||||
|
.bk-act.ghost { color: var(--text-muted); }
|
||||||
|
|
||||||
.bk-empty-state {
|
.bk-empty-state {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -70,16 +70,7 @@ const bausteineState = computed(() => {
|
|||||||
return props.bausteine.ready ? 'done' : 'none'
|
return props.bausteine.ready ? 'done' : 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Re-Run ab gewählter Phase (1-basiert). null = ab Anfang / Fortsetzen.
|
// Re-Run ab Teilschritt passiert jetzt in der Bausteine-Übersicht; die Sidebar hat keine Phasen-Pillen mehr.
|
||||||
const gewaehltePhase = ref(null)
|
|
||||||
// Phasen anklickbar, sobald etwas (teil-)gebaut ist und gerade nicht generiert wird.
|
|
||||||
const phasenWaehlbar = computed(() => (props.bausteine.ready || props.bausteine.partial) && !props.bausteine.generating)
|
|
||||||
const gewaehltesLabel = computed(() => (props.bausteine.steps || [])[(gewaehltePhase.value || 0) - 1]?.label || '')
|
|
||||||
|
|
||||||
function phaseKlick(n) {
|
|
||||||
if (!phasenWaehlbar.value) return
|
|
||||||
gewaehltePhase.value = gewaehltePhase.value === n ? null : n
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
// Nur FREMDE Themen — das gewählte Thema zeigt seinen Fortschritt inline an der Zeile
|
||||||
const activeGenerations = computed(() => {
|
const activeGenerations = computed(() => {
|
||||||
@@ -104,10 +95,9 @@ function confirmResetBausteine() {
|
|||||||
|
|
||||||
function handleBausteinePlay() {
|
function handleBausteinePlay() {
|
||||||
if (bausteineState.value === 'generating') return
|
if (bausteineState.value === 'generating') return
|
||||||
// Nur „Neu generieren" (ready) nutzt eine Phase; Fortsetzen/Erstbau resumen ohne Löschen.
|
// „Neu generieren" (ready) = ab Anfang; Erstbau/Fortsetzen ohne Löschen. Feiner Re-Run via Übersicht.
|
||||||
const abPhase = props.bausteine.ready ? (gewaehltePhase.value ?? 1) : null
|
const abPhase = props.bausteine.ready ? 1 : null
|
||||||
emit('bausteineClick', { instructions: '', abPhase })
|
emit('bausteineClick', { instructions: '', abPhase })
|
||||||
gewaehltePhase.value = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
|
// Name-Klick = Primäraktion: fertige Bausteine → Übersicht, sonst Panel auf/zu.
|
||||||
@@ -389,16 +379,6 @@ function saveQuelle() {
|
|||||||
class="resume-badge"
|
class="resume-badge"
|
||||||
title="Abgebrochen — Fortsetzen möglich"
|
title="Abgebrochen — Fortsetzen möglich"
|
||||||
>Pausiert</span>
|
>Pausiert</span>
|
||||||
<span class="step-dots">
|
|
||||||
<span
|
|
||||||
v-for="(s, i) in (bausteine.steps || [])"
|
|
||||||
:key="s.label"
|
|
||||||
class="step-pill"
|
|
||||||
:class="[s.state, { sel: gewaehltePhase === i + 1, klickbar: phasenWaehlbar }]"
|
|
||||||
:title="(s.state === 'active' ? (bausteine.progress || s.label) : s.label) + (phasenWaehlbar ? ' — Klick: ab hier neu' : '')"
|
|
||||||
@click.stop="phaseKlick(i + 1)"
|
|
||||||
>{{ i + 1 }}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')">▾</button>
|
<button class="panel-toggle" :class="{ open: isOpen('bausteine') }" title="Aktionen" @click.stop="togglePanel('bausteine')">▾</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -407,7 +387,7 @@ function saveQuelle() {
|
|||||||
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
|
<button class="panel-btn danger" :class="{ armed: pendingConfirm === 'bausteine' }" @click="confirmCancelBausteine">{{ pendingConfirm === 'bausteine' ? 'Sicher?' : 'Abbrechen' }}</button>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? (gewaehltePhase ? `Ab «${gewaehltesLabel}» neu` : 'Neu generieren') : 'Generieren' }}</button>
|
<button class="panel-btn play" @click="handleBausteinePlay">{{ bausteine.partial ? 'Fortsetzen' : bausteine.ready ? 'Neu generieren' : 'Generieren' }}</button>
|
||||||
<button
|
<button
|
||||||
v-if="bausteine.ready || bausteine.partial"
|
v-if="bausteine.ready || bausteine.partial"
|
||||||
class="panel-btn danger"
|
class="panel-btn danger"
|
||||||
|
|||||||
Reference in New Issue
Block a user