This commit is contained in:
Team3
2026-06-07 11:29:04 +02:00
parent 6743b1234e
commit 58fb1b6a56
16 changed files with 225 additions and 60 deletions

View File

@@ -15,6 +15,7 @@ TIMEOUTS = {
"recherche": (1800, 0), # fix 30 min "recherche": (1800, 0), # fix 30 min
"auswahl": (600, 10), "auswahl": (600, 10),
"auswahl_check": (300, 2), "auswahl_check": (300, 2),
"ergaenzung": (900, 0), # Themenfeld-Ergänzung bei Projekten (Web-Recherche)
"guide_auswahl": (300, 5), # pro Baustein im Inventar "guide_auswahl": (300, 5), # pro Baustein im Inventar
"guide_check": (300, 2), # Auswahl-/Gliederungs-Prüfung (nur Titellisten) "guide_check": (300, 2), # Auswahl-/Gliederungs-Prüfung (nur Titellisten)
"plan": (300, 5), "plan": (300, 5),

View File

@@ -2,6 +2,7 @@ import asyncio
import json import json
import math import math
import shutil import shutil
import subprocess
import re import re
import uuid import uuid
from datetime import datetime, timezone from datetime import datetime, timezone
@@ -26,7 +27,7 @@ async def cancel_guide(guide_id: str) -> bool:
_cancelled.add(guide_id) _cancelled.add(guide_id)
kill_process(guide_id) kill_process(guide_id)
now = datetime.now(timezone.utc).isoformat() now = datetime.now(timezone.utc).isoformat()
await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen", updated_at=now) await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen — Fortschritt bleibt erhalten", updated_at=now)
return True return True
@@ -201,6 +202,13 @@ BAUSTEINE_STEPS = ("Recherche", "Auswahl", "Prüfung")
_CATEGORIES = ("KERN", "WICHTIG", "REST") # nur noch für den Altformat-Reader _CATEGORIES = ("KERN", "WICHTIG", "REST") # nur noch für den Altformat-Reader
def _bausteine_steps(topic: str) -> tuple:
"""Projekte haben einen 4. Schritt: Themenfeld-Ergänzung per Web-Recherche."""
if project_dir(topic).is_dir():
return BAUSTEINE_STEPS + ("Ergänzung",)
return BAUSTEINE_STEPS
def _bausteine_files(topic: str) -> dict: def _bausteine_files(topic: str) -> dict:
arbeit = arbeit_dir(topic) arbeit = arbeit_dir(topic)
return { return {
@@ -209,11 +217,12 @@ def _bausteine_files(topic: str) -> dict:
"recherche": [arbeit / f"recherche-{i}.md" for i in (1, 2, 3, 4)], "recherche": [arbeit / f"recherche-{i}.md" for i in (1, 2, 3, 4)],
"auswahl": [arbeit / f"auswahl-{i}.md" for i in (1, 2)], "auswahl": [arbeit / f"auswahl-{i}.md" for i in (1, 2)],
"auswahl_check": arbeit / "auswahl-check.json", "auswahl_check": arbeit / "auswahl-check.json",
"ergaenzung": arbeit / "ergaenzung.json",
} }
def _alle_slot_dateien(files: dict) -> list[Path]: def _alle_slot_dateien(files: dict) -> list[Path]:
return [*files["recherche"], *files["auswahl"], files["auswahl_check"]] return [*files["recherche"], *files["auswahl"], files["auswahl_check"], files["ergaenzung"]]
def cancel_bausteine(topic: str) -> bool: def cancel_bausteine(topic: str) -> bool:
@@ -233,10 +242,13 @@ def _resume_step(topic: str) -> int:
return 1 return 1
if not files["auswahl_check"].exists(): if not files["auswahl_check"].exists():
return 2 return 2
if project_dir(topic).is_dir() and not files["ergaenzung"].exists():
return 3 return 3
return len(_bausteine_steps(topic))
def bausteine_status(topic: str) -> dict: def bausteine_status(topic: str) -> dict:
steps = _bausteine_steps(topic)
ready = bausteine_path(topic).exists() ready = bausteine_path(topic).exists()
generating = topic in _bausteine_progress generating = topic in _bausteine_progress
partial = False partial = False
@@ -244,21 +256,21 @@ def bausteine_status(topic: str) -> dict:
current = _bausteine_step.get(topic) current = _bausteine_step.get(topic)
states = [ states = [
"pending" if current is None else "done" if i < current else "active" if i == current else "pending" "pending" if current is None else "done" if i < current else "active" if i == current else "pending"
for i in range(len(BAUSTEINE_STEPS)) for i in range(len(steps))
] ]
elif ready: elif ready:
states = ["done"] * len(BAUSTEINE_STEPS) states = ["done"] * len(steps)
else: else:
nxt = _resume_step(topic) nxt = _resume_step(topic)
partial = nxt > 0 partial = nxt > 0
states = ["done" if i < nxt else "pending" for i in range(len(BAUSTEINE_STEPS))] states = ["done" if i < nxt else "pending" for i in range(len(steps))]
return { return {
"ready": ready, "ready": ready,
"generating": generating, "generating": generating,
"progress": _bausteine_progress.get(topic), "progress": _bausteine_progress.get(topic),
"error": _bausteine_errors.get(topic), "error": _bausteine_errors.get(topic),
"partial": partial, "partial": partial,
"steps": [{"label": label, "state": s} for label, s in zip(BAUSTEINE_STEPS, states)], "steps": [{"label": label, "state": s} for label, s in zip(steps, states)],
} }
@@ -273,6 +285,41 @@ def reset_bausteine(topic: str) -> None:
_bausteine_errors.pop(topic, None) _bausteine_errors.pop(topic, None)
def _ergaenzung_schema(data):
"""{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None."""
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
return None
out = []
for b in data["bausteine"]:
if not isinstance(b, dict) or not isinstance(b.get("titel"), str) or not isinstance(b.get("beschreibung"), str):
return None
titel, beschreibung = b["titel"].strip(), b["beschreibung"].strip()
if not titel:
return None
out.append((titel, beschreibung))
return out
def _pdfs_konvertieren(project: Path) -> None:
"""PDFs im Projekt in .txt wandeln (pdftotext) — Agenten lesen Text statt Seiten-Bildern.
Wird vor jeder Projekt-Generierung aufgerufen; konvertiert nur, wenn die
.txt fehlt oder älter als das PDF ist. Das Original bleibt unangetastet.
"""
if shutil.which("pdftotext") is None:
_log(project.name, "pdftotext nicht installiert — PDFs bleiben unkonvertiert")
return
for pdf in project.rglob("*.pdf"):
txt = pdf.with_suffix(".txt")
if txt.exists() and txt.stat().st_mtime >= pdf.stat().st_mtime:
continue
try:
subprocess.run(["pdftotext", "-layout", str(pdf), str(txt)], check=True, timeout=120)
_log(project.name, f"PDF konvertiert: {pdf.name}{txt.name}")
except Exception as e:
_log(project.name, f"PDF-Konvertierung fehlgeschlagen ({pdf.name}): {e}")
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str = "", project: Path | None = None) -> str: def _build_recherche_prompt(topic: str, out_path: Path, instructions: str = "", project: Path | None = None) -> str:
if project: if project:
source = _prompt("Bausteine-Quelle-Projekt", project=project) source = _prompt("Bausteine-Quelle-Projekt", project=project)
@@ -384,6 +431,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
try: try:
async with _semaphore: async with _semaphore:
files["arbeit"].mkdir(parents=True, exist_ok=True) files["arbeit"].mkdir(parents=True, exist_ok=True)
if project:
await asyncio.to_thread(_pdfs_konvertieren, project)
# „Neu erstellen": fertige Bausteine → kompletter Frischstart. # „Neu erstellen": fertige Bausteine → kompletter Frischstart.
# Sonst sind Slot-Dateien Reste eines Abbruchs/Fehlers → Resume. # Sonst sind Slot-Dateien Reste eines Abbruchs/Fehlers → Resume.
if final_path.exists(): if final_path.exists():
@@ -486,6 +535,40 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
texts = [t for _, t in sorted(entries.items())] + list(patch["nachtraege"]) texts = [t for _, t in sorted(entries.items())] + list(patch["nachtraege"])
entries = {i: t for i, t in enumerate(texts, 1)} entries = {i: t for i, t in enumerate(texts, 1)}
# Schritt 4 (nur Projekte): Themenfeld-Ergänzung — Skript/Projekt ist ein Ausschnitt,
# ein Web-Agent ergänzt kanonisch fehlende Bausteine, markiert mit [Ergänzung].
if project:
set_p("Ergänze Themenfeld…", step=3)
erg_path = files["ergaenzung"]
ergaenzungen = _ergaenzung_schema(_json_datei(erg_path))
if ergaenzungen is None:
erg_path.unlink(missing_ok=True)
slots = [{
"key": f"bausteine-{topic}-ergaenzung-1",
"prompt": _prompt(
"Bausteine-Ergaenzung",
topic=topic, bausteine="\n".join(f"- {t}" for t in entries.values()),
out_path=erg_path, extra=_extra(instructions),
),
"role": "quick", "capabilities": "full",
"payload": (lambda result: _ergaenzung_schema(_json_datei(erg_path))),
}]
res = await _race(topic, "Ergänzung", slots, 1, _timeout("ergaenzung"), provider, cancelled=is_cancelled)
if is_cancelled():
abgebrochen()
return
if res is None:
_bausteine_errors[topic] = "Ergänzung fehlgeschlagen (kein gültiges Ergebnis)"
return
ergaenzungen = res[0]
idx = _titel_index(entries)
neu = [(t, b) for t, b in ergaenzungen if _titel_aufloesen(idx, t) is None]
if neu:
_log(topic, f"Ergänzung: {len(neu)} Baustein(e) aus dem Themenfeld ergänzt")
start = max(entries, default=0) + 1
for off, (t, b) in enumerate(neu):
entries[start + off] = f"{t}{b} [Ergänzung]"
# Titel eindeutig machen und unsortiertes Inventar schreiben # Titel eindeutig machen und unsortiertes Inventar schreiben
entries = _eindeutige_titel(entries) entries = _eindeutige_titel(entries)
final_path.write_text( final_path.write_text(
@@ -1192,6 +1275,9 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
if guide_id in _cancelled: if guide_id in _cancelled:
return return
if project:
await asyncio.to_thread(_pdfs_konvertieren, project)
# „Neu erstellen": fertiger Guide → kompletter Frischstart. # „Neu erstellen": fertiger Guide → kompletter Frischstart.
# Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume. # Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume.
if content_path.exists(): if content_path.exists():

View File

@@ -169,8 +169,10 @@ async def create(req: GuideCreateRequest):
for g in await list_guides(): for g in await list_guides():
if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"): if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"):
raise HTTPException(409, "Generierung läuft bereits") raise HTTPException(409, "Generierung läuft bereits")
# Lernschulden-Regel: neue Guides nur, wenn das Format weniger als 5 offene hat (erstellt, nicht absolviert) # Lernschulden-Regel: neue Guides nur, wenn das Format weniger als 5 offene hat (erstellt, nicht absolviert).
if req.format != "OnePager" and not guide_content_path(req.topic.strip(), req.format).exists(): # Resume (Schritt-Dateien vorhanden) ist ausgenommen — der Guide wurde bereits angefangen.
content = guide_content_path(req.topic.strip(), req.format)
if req.format != "OnePager" and not content.exists() and not guide_slot_dateien(content):
stat = (await _formate_stats()).get(req.format, {"erstellt": 0, "absolviert": 0}) stat = (await _formate_stats()).get(req.format, {"erstellt": 0, "absolviert": 0})
offen = stat["erstellt"] - stat["absolviert"] offen = stat["erstellt"] - stat["absolviert"]
if offen >= MAX_OFFENE_GUIDES: if offen >= MAX_OFFENE_GUIDES:
@@ -240,16 +242,22 @@ async def cancel(guide_id: str):
@router.delete("/guides/{guide_id}") @router.delete("/guides/{guide_id}")
async def remove(guide_id: str): async def remove(guide_id: str, slots: bool = False):
guide = await get_guide(guide_id) guide = await get_guide(guide_id)
if guide is None: if guide is None:
raise HTTPException(404, "Guide nicht gefunden") raise HTTPException(404, "Guide nicht gefunden")
await delete_progress(guide_id)
await delete_guide(guide_id)
# Content-/Schritt-Dateien teilen sich alle Läufe eines Thema+Formats — erst löschen,
# wenn kein Eintrag sie mehr braucht. Teilfortschritt (Schritt-Dateien ohne fertigen
# Content) bleibt fürs Resume erhalten, außer es wird explizit verlangt (slots=1).
rest = [g for g in await list_guides() if g["topic"] == guide["topic"] and g["format"] == guide["format"]]
if not rest:
content = guide_content_path(guide["topic"], guide["format"]) content = guide_content_path(guide["topic"], guide["format"])
if slots or content.exists():
for p in guide_slot_dateien(content): for p in guide_slot_dateien(content):
p.unlink(missing_ok=True) p.unlink(missing_ok=True)
content.unlink(missing_ok=True) content.unlink(missing_ok=True)
await delete_progress(guide_id)
await delete_guide(guide_id)
return {"ok": True} return {"ok": True}

View File

@@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, computed, watch, onMounted, onUnmounted, 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, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats } 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, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats } from './api.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'
@@ -118,9 +118,22 @@ async function loadTopics() {
} }
} }
// Fehlermeldungen verhalten sich wie Flash-Messages: × blendet aus,
// beim Reload sind Alt-Fehler von vornherein ausgeblendet.
const dismissedErrors = ref(new Set())
let errorsInitialized = false
function handleDismissError(guideId) {
dismissedErrors.value = new Set([...dismissedErrors.value, guideId])
}
async function loadGuides() { async function loadGuides() {
try { try {
guides.value = await fetchGuides() guides.value = await fetchGuides()
if (!errorsInitialized) {
errorsInitialized = true
dismissedErrors.value = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
}
loadStats() loadStats()
} catch (e) { } catch (e) {
console.error('Fehler beim Laden:', e) console.error('Fehler beim Laden:', e)
@@ -166,10 +179,16 @@ function selectTopic(topic) {
selectedTopic.value = topic selectedTopic.value = topic
previewGuide.value = null previewGuide.value = null
sidebarSticky.value = false sidebarSticky.value = false
localStorage.setItem('lastTopic', topic)
loadBausteine() loadBausteine()
nextTick(autoPreview) nextTick(autoPreview)
} }
// Beim Reload dort landen, wo man vorher war (Thema + Format)
watch(previewGuide, (g) => {
if (g) localStorage.setItem('lastFormat', g.format)
})
async function createTopic(topic) { async function createTopic(topic) {
await apiCreateTopic(topic) await apiCreateTopic(topic)
await loadTopics() await loadTopics()
@@ -223,8 +242,8 @@ function handlePreview(guide) {
previewGuide.value = guide previewGuide.value = guide
} }
async function handleDeleteGuide(guideId) { async function handleDeleteGuide(guideId, slots = false) {
await deleteGuide(guideId) await deleteGuide(guideId, slots)
if (previewGuide.value?.id === guideId) { if (previewGuide.value?.id === guideId) {
previewGuide.value = null previewGuide.value = null
} }
@@ -278,7 +297,14 @@ function onVisibility() {
onMounted(async () => { onMounted(async () => {
await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()]) await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()])
if (!selectedTopic.value && topics.value.length) { const savedTopic = localStorage.getItem('lastTopic')
const savedFormat = localStorage.getItem('lastFormat')
if (savedTopic && [...topics.value, ...projectNames.value].includes(savedTopic)) {
selectTopic(savedTopic)
await nextTick()
const g = doneByFormat.value[savedFormat]
if (g) previewGuide.value = g
} else if (!selectedTopic.value && topics.value.length) {
selectTopic(topics.value[0]) selectTopic(topics.value[0])
} }
document.addEventListener('visibilitychange', onVisibility) document.addEventListener('visibilitychange', onVisibility)
@@ -302,6 +328,7 @@ onUnmounted(() => {
:doneByFormat="doneByFormat" :doneByFormat="doneByFormat"
:latestByFormat="latestByFormat" :latestByFormat="latestByFormat"
:allGuides="guides" :allGuides="guides"
:dismissedErrors="dismissedErrors"
:bausteine="bausteine" :bausteine="bausteine"
:activeBausteine="activeBausteine" :activeBausteine="activeBausteine"
:pinned="sidebarPinned" :pinned="sidebarPinned"
@@ -320,6 +347,7 @@ onUnmounted(() => {
@deleteProject="handleDeleteProject" @deleteProject="handleDeleteProject"
@cancelGuide="handleCancel" @cancelGuide="handleCancel"
@deleteGuide="handleDeleteGuide" @deleteGuide="handleDeleteGuide"
@dismissError="handleDismissError"
@preview="handlePreview" @preview="handlePreview"
@togglePin="toggleSidebarPin" @togglePin="toggleSidebarPin"
@sidebarLeave="onSidebarLeave" @sidebarLeave="onSidebarLeave"

View File

@@ -64,8 +64,8 @@ export async function cancelGuide(id) {
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' }) await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
} }
export async function deleteGuide(id) { export async function deleteGuide(id, slots = false) {
await fetch(`${BASE}/guides/${id}`, { method: 'DELETE' }) await fetch(`${BASE}/guides/${id}${slots ? '?slots=1' : ''}`, { method: 'DELETE' })
} }
export async function fetchGuideContent(id) { export async function fetchGuideContent(id) {

View File

@@ -48,6 +48,7 @@ const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366
// --- Inhalt laden --- // --- Inhalt laden ---
const content = ref(null) const content = ref(null)
const loadError = ref(null)
const doneChapters = ref(new Set()) const doneChapters = ref(new Set())
const scrollEl = ref(null) const scrollEl = ref(null)
@@ -55,6 +56,7 @@ watch(() => props.previewGuide?.id, loadContent, { immediate: true })
async function loadContent() { async function loadContent() {
content.value = null content.value = null
loadError.value = null
doneChapters.value = new Set() doneChapters.value = new Set()
const g = props.previewGuide const g = props.previewGuide
if (!g || g.status !== 'done') return if (!g || g.status !== 'done') return
@@ -62,6 +64,7 @@ async function loadContent() {
content.value = await fetchGuideContent(g.id) content.value = await fetchGuideContent(g.id)
} catch (e) { } catch (e) {
console.error('Fehler beim Laden des Guides:', e) console.error('Fehler beim Laden des Guides:', e)
loadError.value = 'Inhalt nicht verfügbar — die Datei fehlt. Guide neu generieren (▶).'
return return
} }
try { try {
@@ -236,7 +239,7 @@ async function send() {
</div> </div>
<div v-else-if="previewGuide" class="empty-preview"> <div v-else-if="previewGuide" class="empty-preview">
<p>Lade Inhalt</p> <p>{{ loadError || 'Lade Inhalt…' }}</p>
</div> </div>
<div class="empty-preview" v-else> <div class="empty-preview" v-else>

View File

@@ -9,6 +9,7 @@ const props = defineProps({
doneByFormat: { type: Object, default: () => ({}) }, doneByFormat: { type: Object, default: () => ({}) },
latestByFormat: { type: Object, default: () => ({}) }, latestByFormat: { type: Object, default: () => ({}) },
allGuides: { type: Array, default: () => [] }, allGuides: { type: Array, default: () => [] },
dismissedErrors: { type: Object, default: () => new Set() },
bausteine: { type: Object, default: () => ({ ready: false, generating: false, progress: null, error: null }) }, bausteine: { type: Object, default: () => ({ ready: false, generating: false, progress: null, error: null }) },
activeBausteine: { type: Array, default: () => [] }, activeBausteine: { type: Array, default: () => [] },
pinned: { type: Boolean, default: true }, pinned: { type: Boolean, default: true },
@@ -17,7 +18,7 @@ const props = defineProps({
providers: { type: Array, default: () => [] }, providers: { type: Array, default: () => [] },
}) })
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider']) const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
function providerAvailable(id) { function providerAvailable(id) {
const p = props.providers.find((x) => x.id === id) const p = props.providers.find((x) => x.id === id)
@@ -104,21 +105,38 @@ function guideStatus(format) {
const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung'] const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung']
const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung'] const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung']
// Kugeln werden wie bei den Bausteinen immer angezeigt:
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
function guideSteps(format) { function guideSteps(format) {
const st = guideStatus(format)
if (st !== 'generating' && st !== 'queued') return []
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
const st = guideStatus(format)
if (st === 'generating' || st === 'queued') {
const step = props.latestByFormat[format]?.step ?? -1 const step = props.latestByFormat[format]?.step ?? -1
return labels.map((label, i) => ({ return labels.map((label, i) => ({
label, label,
state: i < step ? 'done' : i === step ? 'active' : 'pending', state: i < step ? 'done' : i === step ? 'active' : 'pending',
})) }))
}
if (props.doneByFormat[format]) {
return labels.map((label) => ({ label, state: 'done' }))
}
if (abgebrochen(format)) {
const step = props.latestByFormat[format]?.step ?? 0
return labels.map((label, i) => ({ label, state: i < step ? 'done' : 'pending' }))
}
return labels.map((label) => ({ label, state: 'pending' }))
} }
function errorMsg(format) { function errorMsg(format) {
const latest = props.latestByFormat[format] const latest = props.latestByFormat[format]
if (latest?.status === 'error') return latest.error_msg || 'Fehler bei der Generierung' if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
return '' return latest.error_msg || 'Fehler bei der Generierung'
}
// Abgebrochener Lauf = Teilfortschritt vorhanden: ▶ setzt fort, ✕ löscht den Fortschritt
function abgebrochen(format) {
const latest = props.latestByFormat[format]
return latest?.status === 'error' && (latest.error_msg || '').startsWith('Abgebrochen')
} }
function handleFormatClick(format) { function handleFormatClick(format) {
@@ -150,11 +168,10 @@ function handlePlay(format) {
emit('formatClick', { format, instructions: '' }) emit('formatClick', { format, instructions: '' })
} }
// Flash-Message-Verhalten: × blendet nur aus, nichts wird gelöscht
function dismissError(format) { function dismissError(format) {
const latest = props.latestByFormat[format] const latest = props.latestByFormat[format]
if (latest?.status === 'error') { if (latest?.status === 'error') emit('dismissError', latest.id)
emit('deleteGuide', latest.id)
}
} }
function handleDelete(format) { function handleDelete(format) {
@@ -297,7 +314,7 @@ function confirmDeleteProject(name) {
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'"> <template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
<button <button
class="action-btn play" class="action-btn play"
:title="playLock(f.key) || 'Generieren'" :title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
:disabled="!!playLock(f.key)" :disabled="!!playLock(f.key)"
@click="handlePlay(f.key)" @click="handlePlay(f.key)"
></button> ></button>
@@ -306,7 +323,7 @@ function confirmDeleteProject(name) {
</div> </div>
<div v-if="errorMsg(f.key)" class="format-error"> <div v-if="errorMsg(f.key)" class="format-error">
<span class="format-error-text">{{ errorMsg(f.key) }}</span> <span class="format-error-text">{{ errorMsg(f.key) }}</span>
<button class="format-error-x" title="Fehler entfernen" @click="dismissError(f.key)">&times;</button> <button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
</div> </div>
</div> </div>
</div> </div>
@@ -664,6 +681,7 @@ function confirmDeleteProject(name) {
} }
.format-x.armed, .format-x.armed,
.format-error-x.armed,
.delete-topic.armed { .delete-topic.armed {
display: inline-block; display: inline-block;
font-size: 0.7rem; font-size: 0.7rem;

View File

@@ -0,0 +1,21 @@
Prüfe das Baustein-Inventar zum Thema "{topic}" auf Vollständigkeit gegenüber dem Themenfeld.
Das Inventar stammt aus einem Projekt/Skript — es kann Bausteine geben, die fachlich zum Thema gehören, dort aber nicht behandelt werden. Finde genau diese Lücken.
VORHANDENE BAUSTEINE:
{bausteine}
Regeln:
- Recherchiere das Themenfeld (Lehrbücher, Standardreferenzen) und ergänze NUR Bausteine, die kanonisch dazugehören und im Inventar fehlen.
- Ein Baustein löst GENAU EIN PROBLEM und ist ATOMAR — gleiche Maßstäbe wie im Inventar.
- KEINE Varianten, Umformulierungen oder Vertiefungen vorhandener Bausteine — nur echte Lücken.
- Erfinde nichts: nur Bausteine, die du in der Recherche belegt hast.
- Titel und Beschreibung auf DEUTSCH (Fachbegriffe bleiben original), Beschreibung maximal ~12 Wörter.
- Gibt es keine Lücken, liefere eine leere Liste — das ist ein gültiges Ergebnis.
Schreibe NUR die JSON-Datei nach: {out_path}
Format:
{{"bausteine": [{{"titel": "…", "beschreibung": "…"}}]}}
Keine Lücken: {{"bausteine": []}}
{extra}

View File

@@ -1 +1 @@
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku-Ordner und den relevanten Quellcode mit dem Read-Tool. Die Bausteine müssen das echte Projekt widerspiegeln, nichts Erfundenes. Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku-Ordner und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. Die Bausteine müssen das echte Projekt widerspiegeln, nichts Erfundenes.

View File

@@ -1 +1 @@
Die Fakten stammen aus dem Projekt unter {project} — lies bei Bedarf Dateien mit Read/Bash nach. Die Fakten stammen aus dem Projekt unter {project} — lies bei Bedarf Dateien mit Read/Bash nach. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF.

View File

@@ -5,12 +5,12 @@ FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
Erstelle GENAU diese 7 Karten (JSON-Schlüssel exakt so): Erstelle GENAU diese 7 Karten (JSON-Schlüssel exakt so):
- "info" — Titel: "{topic}". Kurzbeschreibung in 12 Sätzen, darunter technische Daten als Stichpunkte (Art/Typ, Version/Stand, Lizenz/Kosten). - "info" — Titel: "{topic}". Kurzbeschreibung in 12 Sätzen, darunter technische Daten als Stichpunkte (Art/Typ, Version/Stand, Lizenz/Kosten).
- "eigenschaften" — Titel: "Kerneigenschaften". Die 47 prägenden Eigenschaften des Systems als Stichpunkte. - "eigenschaften" — Titel: "Kerneigenschaften". Was einen IM Thema erwartet: kleine Übersicht der Inhalte/Teilgebiete.
- "beispiel" — Titel: "Beispiel". EIN anschauliches, typisches Codebeispiel (Markdown-Codeblock) mit einem Satz Erklärung. - "beispiel" — Titel: "Beispiel". EIN anschauliches, typisches Codebeispiel (Markdown-Codeblock) mit einem Satz Erklärung.
- "zusammenhaenge" — Titel: "Zusammenhänge". Mit welchen Systemen/Themen es zusammenhängt — Stichpunkte mit je einem halben Satz. - "zusammenhaenge" — Titel: "Zusammenhänge". Mit welchen ANDEREN Themen es zusammenhängt — Nachbarthemen außerhalb dieses Themas, keine Inhalte des Themas selbst.
- "voraussetzungen" — Titel: "Voraussetzungen". Was man vorher können oder haben muss. - "voraussetzungen" — Titel: "Voraussetzungen". Welche Themen man vorher bearbeitet haben sollte, um hier klarzukommen.
- "modern" — Titel: "Moderne Features". Was aktuell ist und heute verwendet wird. - "modern" — Titel: "Moderne Features". NUR was in den letzten Jahren neu dazugekommen ist. Gibt es nichts Neues: ehrlich "Keine." mit einem Satz Begründung.
- "veraltet" — Titel: "Veraltete Features". Was es noch gibt, aber nicht mehr verwendet werden sollte. Gibt es nichts Veraltetes: ehrlich "Keine." mit einem Satz Begründung — nichts erfinden. - "veraltet" — Titel: "Veraltete Features". Was nicht mehr verwendet wird. Gibt es nichts Veraltetes: ehrlich "Keine." mit einem Satz Begründung — nichts erfinden.
KOMPAKTHEIT — der OnePager muss OHNE Scrollen auf eine Bildschirmseite passen: KOMPAKTHEIT — der OnePager muss OHNE Scrollen auf eine Bildschirmseite passen:
- Maximal 5 Stichpunkte pro Karte, je maximal ~8 Wörter (Schlagwort + halber Satz). - Maximal 5 Stichpunkte pro Karte, je maximal ~8 Wörter (Schlagwort + halber Satz).

View File

@@ -1 +1 @@
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku und den relevanten Quellcode mit dem Read-Tool. Erfasse Zweck, Architektur und die wichtigsten Konzepte — nichts Erfundenes. Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. Erfasse Zweck, Architektur und die wichtigsten Konzepte — nichts Erfundenes.

View File

@@ -6,11 +6,11 @@ FAKTENBASIS:
Sie muss diese Dimensionen abdecken: Sie muss diese Dimensionen abdecken:
1. Kurzbeschreibung (Art des Projekts, Gegenstand) 1. Kurzbeschreibung (Art des Projekts, Gegenstand)
2. Technische Daten (Technologie/Format, Umfang, Stand/Aktualität) 2. Technische Daten (Technologie/Format, Umfang, Stand/Aktualität)
3. Kerneigenschaften (prägende Konzepte, Komponenten oder Inhalte) 3. Inhaltsübersicht (was einen im Projekt erwartet)
4. Ein typisches Beispiel aus dem Projekt 4. Ein typisches Beispiel aus dem Projekt
5. Zusammenhänge (Umfeld, Abhängigkeiten, angrenzende Systeme/Themen) 5. Zusammenhänge mit ANDEREN Themen (Nachbarthemen außerhalb des Projektinhalts)
6. Voraussetzungen 6. Voraussetzungen (vorher zu bearbeitende Themen)
7. Moderne vs. veraltete Teile (oder die ausdrückliche Feststellung, dass nichts veraltet ist) 7. Neuerungen der letzten Jahre vs. nicht mehr Verwendetes (oder die ausdrückliche Feststellung, dass es jeweils nichts gibt)
Prüfe: Prüfe:
1. Ist jede Dimension mit konkreten Fakten aus den Projektdateien belegt (Namen, Zahlen — nicht vage)? 1. Ist jede Dimension mit konkreten Fakten aus den Projektdateien belegt (Namen, Zahlen — nicht vage)?

View File

@@ -6,11 +6,11 @@ FAKTENBASIS:
Sie muss diese Dimensionen abdecken: Sie muss diese Dimensionen abdecken:
1. Kurzbeschreibung (12 Sätze) 1. Kurzbeschreibung (12 Sätze)
2. Technische Daten (Art/Typ, Version/Stand, Lizenz/Kosten) 2. Technische Daten (Art/Typ, Version/Stand, Lizenz/Kosten)
3. Kerneigenschaften des Systems 3. Inhaltsübersicht (was einen im Thema erwartet)
4. Ein typisches Beispiel 4. Ein typisches Beispiel
5. Zusammenhänge mit anderen Systemen/Themen 5. Zusammenhänge mit ANDEREN Themen (Nachbarthemen, nicht Inhalte des Themas selbst)
6. Voraussetzungen 6. Voraussetzungen (vorher zu bearbeitende Themen)
7. Moderne vs. veraltete Features (oder die ausdrückliche Feststellung, dass nichts veraltet ist) 7. Neuerungen der letzten Jahre vs. nicht mehr Verwendetes (oder die ausdrückliche Feststellung, dass es jeweils nichts gibt)
Prüfe: Prüfe:
1. Ist jede Dimension mit konkreten Fakten belegt (Namen, Versionen, Zahlen — nicht vage)? 1. Ist jede Dimension mit konkreten Fakten belegt (Namen, Versionen, Zahlen — nicht vage)?

View File

@@ -5,11 +5,11 @@ Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Se
Erfasse gezielt diese Dimensionen: Erfasse gezielt diese Dimensionen:
1. Kurzbeschreibung: Was ist "{topic}" in 12 Sätzen (Art des Projekts, Gegenstand)? 1. Kurzbeschreibung: Was ist "{topic}" in 12 Sätzen (Art des Projekts, Gegenstand)?
2. Technische Daten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität. 2. Technische Daten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
3. Kerneigenschaften: die prägenden Konzepte, Komponenten oder Inhalte des Projekts. 3. Inhaltsübersicht: Was erwartet einen — die wichtigsten Inhalte/Teilgebiete des Projekts.
4. Beispiel: ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt). 4. Beispiel: ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
5. Zusammenhänge: in welchem Umfeld es steht (Abhängigkeiten, angrenzende Systeme/Themen). 5. Zusammenhänge: mit welchen ANDEREN Themen es zusammenhängt (Nachbarthemen außerhalb des Projektinhalts).
6. Voraussetzungen: was man können oder haben muss, um es zu nutzen bzw. zu verstehen. 6. Voraussetzungen: welche Themen man vorher bearbeitet haben sollte, um das Projekt zu verstehen.
7. Moderne vs. veraltete Teile: was aktueller Stand ist — und was als Altlast gilt (falls nichts veraltet ist, ausdrücklich notieren). 7. Neuerungen vs. Veraltetes: was im Themenfeld in den letzten Jahren neu dazugekommen ist — und was nicht mehr verwendet wird (falls es nichts gibt, jeweils ausdrücklich notieren).
Schreibe NUR die Markdown-Datei nach: {out_path} Schreibe NUR die Markdown-Datei nach: {out_path}

View File

@@ -5,11 +5,11 @@ Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Se
Recherchiere gezielt diese Dimensionen: Recherchiere gezielt diese Dimensionen:
1. Kurzbeschreibung: Was ist "{topic}" in 12 Sätzen? 1. Kurzbeschreibung: Was ist "{topic}" in 12 Sätzen?
2. Technische Daten: Art/Typ, aktuelle Version/Stand, Lizenz/Kosten, Verbreitung. 2. Technische Daten: Art/Typ, aktuelle Version/Stand, Lizenz/Kosten, Verbreitung.
3. Kerneigenschaften: die prägenden Eigenschaften und Merkmale des Systems. 3. Inhaltsübersicht: Was erwartet einen im Thema — die wichtigsten Inhalte/Teilgebiete.
4. Beispiel: ein minimales, typisches Code-/Anwendungsbeispiel. 4. Beispiel: ein minimales, typisches Code-/Anwendungsbeispiel.
5. Zusammenhänge: mit welchen Systemen/Themen es zusammenhängt (Ökosystem, Nachbarn, typische Kombinationen). 5. Zusammenhänge: mit welchen ANDEREN Themen es zusammenhängt (Nachbarthemen außerhalb von "{topic}").
6. Voraussetzungen: was man vorher können oder haben muss. 6. Voraussetzungen: welche Themen man vorher bearbeitet haben sollte.
7. Moderne vs. veraltete Features: was heute verwendet wird — und was es noch gibt, aber nicht mehr verwendet werden sollte (falls nichts veraltet ist, ausdrücklich notieren). 7. Neuerungen vs. Veraltetes: was in den letzten Jahren neu dazugekommen ist — und was nicht mehr verwendet wird (falls es nichts gibt, jeweils ausdrücklich notieren).
Schreibe NUR die Markdown-Datei nach: {out_path} Schreibe NUR die Markdown-Datei nach: {out_path}