Optionaler Stopp nach der Gliederung je Version

Häkchen beim Anlegen und bei „Neue Version“: der Lauf hält nach der
Gliederung als „pausiert“ an, damit man sie prüfen kann, bevor die
Kapitel geschrieben werden. „Fortsetzen“ schreibt weiter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-12 15:52:14 +02:00
parent 78a74d140b
commit 4cd4b7ed2e
8 changed files with 31 additions and 15 deletions

5
API.md
View File

@@ -17,9 +17,10 @@ Ohne `v2` gilt die neueste Version.
- `POST /api/themen` `{name, hinweise, spezialisierung}` → Thema wie oben (legt `v1` an) - `POST /api/themen` `{name, hinweise, spezialisierung}` → Thema wie oben (legt `v1` an)
– `name`: Hauptthema; `spezialisierung`: grenzt es ein, nur was dafür gebraucht wird; die Bezeichnung (`name` in der Antwort, Ordner) wird „Thema – Spezialisierung“ – `name`: Hauptthema; `spezialisierung`: grenzt es ein, nur was dafür gebraucht wird; die Bezeichnung (`name` in der Antwort, Ordner) wird „Thema – Spezialisierung“
– `hinweise`: wie das Thema zu verstehen ist; gelten je Version und sind bei einer neuen Version änderbar – `hinweise`: wie das Thema zu verstehen ist; gelten je Version und sind bei einer neuen Version änderbar
– `stopp_gliederung` (bool, optional): der Lauf hält nach der Gliederung an (`pausiert`); „Fortsetzen“ schreibt die Kapitel
- `GET /api/themen/{slug}` → Thema wie oben - `GET /api/themen/{slug}` → Thema wie oben
- `DELETE /api/themen/{slug}` – ganzes Thema - `DELETE /api/themen/{slug}` – ganzes Thema
- `POST /api/themen/{slug}/versionen` `{hinweise?}` → `{"v": N}` – nächste Version ohne Start; ohne Angabe übernimmt sie die Hinweise der neuesten Version - `POST /api/themen/{slug}/versionen` `{hinweise?, stopp_gliederung?}` → `{"v": N}` – nächste Version ohne Start; ohne Angabe übernimmt sie die Hinweise der neuesten Version
- `DELETE /api/themen/{slug}/v{N}` – nur diese Version - `DELETE /api/themen/{slug}/v{N}` – nur diese Version
- `POST /api/themen/{slug}/v{N}/start` – startet oder setzt fort (resümiert an der offenen Stufe) - `POST /api/themen/{slug}/v{N}/start` – startet oder setzt fort (resümiert an der offenen Stufe)
- `POST /api/themen/{slug}/v{N}/pause` – bricht laufende Agenten ab; Neustart mit `start` - `POST /api/themen/{slug}/v{N}/pause` – bricht laufende Agenten ab; Neustart mit `start`
@@ -95,7 +96,7 @@ Solange ein Lauf läuft, enthält `buch` die bereits geschriebenen Kapitel (Stat
storage/<Thema>/ storage/<Thema>/
thema.json {name, thema, spezialisierung, slug, titel, hinweise, angelegt} – name = „Thema – Spezialisierung“ thema.json {name, thema, spezialisierung, slug, titel, hinweise, angelegt} – name = „Thema – Spezialisierung“
v1/, v2/, … je Version: v1/, v2/, … je Version:
state.json Zustand wie oben (ohne Buch), mit den Hinweisen der Version state.json Zustand wie oben (ohne Buch), mit hinweise und stopp_gliederung der Version
deutung.json vom Recherche-Agenten deutung.json vom Recherche-Agenten
position.json Leseposition position.json Leseposition
quellen/ QUELLEN.md + geklonte/geladene Quellen quellen/ QUELLEN.md + geklonte/geladene Quellen

View File

@@ -30,10 +30,12 @@ class ThemaNeu(BaseModel):
name: str name: str
hinweise: str = "" hinweise: str = ""
spezialisierung: str = "" spezialisierung: str = ""
stopp_gliederung: bool = False
class VersionNeu(BaseModel): class VersionNeu(BaseModel):
hinweise: str | None = None # None: wie die bisherige Version hinweise: str | None = None # None: wie die bisherige Version
stopp_gliederung: bool = False
class Position(BaseModel): class Position(BaseModel):
@@ -89,7 +91,7 @@ async def thema_anlegen(body: ThemaNeu):
if not state.name_ok(name) or not state.name_ok(state.bezeichnung(name, body.spezialisierung.strip())): if not state.name_ok(name) or not state.name_ok(state.bezeichnung(name, body.spezialisierung.strip())):
raise HTTPException(400, "Ungültiger Name") raise HTTPException(400, "Ungültiger Name")
try: try:
return _thema_info(state.anlegen(name, body.hinweise, body.spezialisierung)) return _thema_info(state.anlegen(name, body.hinweise, body.spezialisierung, body.stopp_gliederung))
except ValueError as e: except ValueError as e:
raise HTTPException(409, str(e)) raise HTTPException(409, str(e))
@@ -112,7 +114,7 @@ async def thema_loeschen(slug: str):
async def version_anlegen(slug: str, body: VersionNeu | None = None): async def version_anlegen(slug: str, body: VersionNeu | None = None):
"""Neue Version: frischer Lauf ohne Wissen über die Vorgänger; Hinweise wahlweise geändert.""" """Neue Version: frischer Lauf ohne Wissen über die Vorgänger; Hinweise wahlweise geändert."""
b = body or VersionNeu() b = body or VersionNeu()
key = state.version_anlegen(_thema(slug)["name"], b.hinweise) key = state.version_anlegen(_thema(slug)["name"], b.hinweise, b.stopp_gliederung)
return {"v": int(key.rsplit("/v", 1)[1])} return {"v": int(key.rsplit("/v", 1)[1])}

View File

@@ -50,6 +50,9 @@ async def _lauf(name: str) -> None:
await STUFEN_FN[sid](name) await STUFEN_FN[sid](name)
s.update(status="fertig", beendet=state.jetzt()) s.update(status="fertig", beendet=state.jetzt())
state.speichern(name) state.speichern(name)
if sid == "gliederung" and st.get("stopp_gliederung"):
st.update(status="pausiert", grund="Nach der Gliederung angehalten – „Fortsetzen“ schreibt die Kapitel", stufe="")
return
st.update(status="fertig", grund="", stufe="") st.update(status="fertig", grund="", stufe="")
except asyncio.CancelledError: except asyncio.CancelledError:
st.update(status="pausiert", grund="Pausiert") st.update(status="pausiert", grund="Pausiert")

View File

@@ -55,15 +55,17 @@ def name_von_slug(sl: str) -> str | None:
return next((t["name"] for t in themen() if t.get("slug") == sl), None) return next((t["name"] for t in themen() if t.get("slug") == sl), None)
def version_anlegen(name: str, hinweise: str | None = None) -> str: def version_anlegen(name: str, hinweise: str | None = None, stopp_gliederung: bool = False) -> str:
"""Neue Version; ohne Angabe übernimmt sie die Hinweise der neuesten Version (bzw. des Themas).""" """Neue Version; ohne Angabe übernimmt sie die Hinweise der neuesten Version (bzw. des Themas).
stopp_gliederung: der Lauf hält nach der Gliederung an (günstiger Testlauf), „Fortsetzen“ schreibt weiter."""
vs = versionen(name) vs = versionen(name)
alt = hinweise_von(f"{name}/v{vs[-1]}") if vs else (thema(name) or {}).get("hinweise", "") alt = hinweise_von(f"{name}/v{vs[-1]}") if vs else (thema(name) or {}).get("hinweise", "")
key = f"{name}/v{(vs or [0])[-1] + 1}" key = f"{name}/v{(vs or [0])[-1] + 1}"
v = verzeichnis(key) v = verzeichnis(key)
for u in ("quellen", "buch", "berichte", "arbeit"): for u in ("quellen", "buch", "berichte", "arbeit"):
(v / u).mkdir(parents=True) (v / u).mkdir(parents=True)
_schreiben(v / "state.json", {**leerer_zustand(), "hinweise": (alt if hinweise is None else hinweise).strip()}) _schreiben(v / "state.json", {**leerer_zustand(), "hinweise": (alt if hinweise is None else hinweise).strip(),
"stopp_gliederung": stopp_gliederung})
return key return key
@@ -137,7 +139,7 @@ def themen() -> list[dict]:
return aus return aus
def anlegen(name: str, hinweise: str, spezialisierung: str = "") -> dict: def anlegen(name: str, hinweise: str, spezialisierung: str = "", stopp_gliederung: bool = False) -> dict:
"""name ist das Hauptthema („Game Design“), die Spezialisierung grenzt es ein; beides zusammen bildet die Bezeichnung.""" """name ist das Hauptthema („Game Design“), die Spezialisierung grenzt es ein; beides zusammen bildet die Bezeichnung."""
spezialisierung = spezialisierung.strip() spezialisierung = spezialisierung.strip()
bez = bezeichnung(name, spezialisierung) bez = bezeichnung(name, spezialisierung)
@@ -148,7 +150,7 @@ def anlegen(name: str, hinweise: str, spezialisierung: str = "") -> dict:
t = {"name": bez, "thema": name, "spezialisierung": spezialisierung, "slug": slug_frei(bez), "titel": bez, t = {"name": bez, "thema": name, "spezialisierung": spezialisierung, "slug": slug_frei(bez), "titel": bez,
"hinweise": hinweise.strip(), "angelegt": jetzt()} "hinweise": hinweise.strip(), "angelegt": jetzt()}
_schreiben(d / "thema.json", t) _schreiben(d / "thema.json", t)
version_anlegen(bez) version_anlegen(bez, stopp_gliederung=stopp_gliederung)
return t return t
@@ -160,14 +162,14 @@ def loeschen(name: str) -> None:
def zuruecksetzen(key: str) -> None: def zuruecksetzen(key: str) -> None:
v = verzeichnis(key) v = verzeichnis(key)
h = hinweise_von(key) h, stopp = hinweise_von(key), bool(zustand(key).get("stopp_gliederung"))
_cache.pop(key, None) _cache.pop(key, None)
for u in ("quellen", "buch", "berichte", "arbeit"): for u in ("quellen", "buch", "berichte", "arbeit"):
shutil.rmtree(v / u, ignore_errors=True) shutil.rmtree(v / u, ignore_errors=True)
(v / u).mkdir() (v / u).mkdir()
for f in ("deutung.json", "log.jsonl", "state.json", "position.json"): for f in ("deutung.json", "log.jsonl", "state.json", "position.json"):
(v / f).unlink(missing_ok=True) (v / f).unlink(missing_ok=True)
_schreiben(v / "state.json", {**leerer_zustand(), "hinweise": h}) _schreiben(v / "state.json", {**leerer_zustand(), "hinweise": h, "stopp_gliederung": stopp})
def thema(key: str) -> dict | None: def thema(key: str) -> dict | None:

View File

@@ -36,6 +36,7 @@ const fehler = ref('')
const neuName = ref('') const neuName = ref('')
const neuHinweise = ref('') const neuHinweise = ref('')
const neuSpezialisierung = ref('') const neuSpezialisierung = ref('')
const neuStopp = ref(false)
const navAuf = ref(false) const navAuf = ref(false)
let timer = null let timer = null
@@ -105,10 +106,11 @@ async function anlegen() {
const name = neuName.value.trim() const name = neuName.value.trim()
if (!name) return if (!name) return
try { try {
const t = await api.anlegen(name, neuHinweise.value.trim(), neuSpezialisierung.value.trim()) const t = await api.anlegen(name, neuHinweise.value.trim(), neuSpezialisierung.value.trim(), neuStopp.value)
neuName.value = '' neuName.value = ''
neuHinweise.value = '' neuHinweise.value = ''
neuSpezialisierung.value = '' neuSpezialisierung.value = ''
neuStopp.value = false
await topicsLaden() await topicsLaden()
themaWaehlen(t.slug) themaWaehlen(t.slug)
} catch (e) { fehler.value = String(e.message || e) } } catch (e) { fehler.value = String(e.message || e) }
@@ -173,6 +175,7 @@ function tokens(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + 'M' : n >= 1e3 ? M
<input v-model="neuName" placeholder="Thema, z. B. Godot 4" @keyup.enter="anlegen" /> <input v-model="neuName" placeholder="Thema, z. B. Godot 4" @keyup.enter="anlegen" />
<input v-model="neuHinweise" placeholder="Hinweise (optional), z. B. für Entwickler, ab Version 4.6" @keyup.enter="anlegen" /> <input v-model="neuHinweise" placeholder="Hinweise (optional), z. B. für Entwickler, ab Version 4.6" @keyup.enter="anlegen" />
<input v-model="neuSpezialisierung" placeholder="Spezialisierung (optional), z. B. Kartenspiel" title="Grenzt das Thema ein; Bezeichnung wird „Thema – Spezialisierung“" @keyup.enter="anlegen" /> <input v-model="neuSpezialisierung" placeholder="Spezialisierung (optional), z. B. Kartenspiel" title="Grenzt das Thema ein; Bezeichnung wird „Thema – Spezialisierung“" @keyup.enter="anlegen" />
<label class="haken" title="Günstiger Testlauf: Recherche und Gliederung, dann Pause"><input type="checkbox" v-model="neuStopp" /> Nach der Gliederung anhalten</label>
<button @click="anlegen">Anlegen</button> <button @click="anlegen">Anlegen</button>
</div> </div>
</aside> </aside>

View File

@@ -18,7 +18,7 @@ const e = encodeURIComponent
const v = (slug, n) => `/themen/${e(slug)}/v${n}` const v = (slug, n) => `/themen/${e(slug)}/v${n}`
export const api = { export const api = {
themen: () => req('/themen'), themen: () => req('/themen'),
anlegen: (name, hinweise = '', spezialisierung = '') => req('/themen', { method: 'POST', body: { name, hinweise, spezialisierung } }), anlegen: (name, hinweise = '', spezialisierung = '', stopp_gliederung = false) => req('/themen', { method: 'POST', body: { name, hinweise, spezialisierung, stopp_gliederung } }),
loeschen: (slug) => req(`/themen/${e(slug)}`, { method: 'DELETE' }), loeschen: (slug) => req(`/themen/${e(slug)}`, { method: 'DELETE' }),
versionAnlegen: (slug, eingaben) => req(`/themen/${e(slug)}/versionen`, { method: 'POST', body: eingaben }), versionAnlegen: (slug, eingaben) => req(`/themen/${e(slug)}/versionen`, { method: 'POST', body: eingaben }),
versionLoeschen: (slug, n) => req(v(slug, n), { method: 'DELETE' }), versionLoeschen: (slug, n) => req(v(slug, n), { method: 'DELETE' }),

View File

@@ -39,8 +39,9 @@ function reset() { if (confirm('Lauf zurücksetzen? Alles außer dem Thema wird
// Neue Version: Formular mit den Hinweisen dieser Version vorbelegt; Thema und Spezialisierung bleiben. // Neue Version: Formular mit den Hinweisen dieser Version vorbelegt; Thema und Spezialisierung bleiben.
const neuOffen = ref(false) const neuOffen = ref(false)
const neuHinweise = ref('') const neuHinweise = ref('')
function neuOeffnen() { neuHinweise.value = props.state.hinweise || ''; neuOffen.value = true } const neuStopp = ref(false)
function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise: neuHinweise.value.trim() }) } function neuOeffnen() { neuHinweise.value = props.state.hinweise || ''; neuStopp.value = !!props.state.stopp_gliederung; neuOffen.value = true }
function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise: neuHinweise.value.trim(), stopp_gliederung: neuStopp.value }) }
</script> </script>
<template> <template>
@@ -60,6 +61,7 @@ function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise:
<div v-if="neuOffen" class="neu-version"> <div v-if="neuOffen" class="neu-version">
<div class="titel">Neue Version von „{{ state.name }}“</div> <div class="titel">Neue Version von „{{ state.name }}“</div>
<input v-model="neuHinweise" placeholder="Hinweise (optional), z. B. für Entwickler, ab Version 4.6" @keyup.enter="neuStarten" /> <input v-model="neuHinweise" placeholder="Hinweise (optional), z. B. für Entwickler, ab Version 4.6" @keyup.enter="neuStarten" />
<label class="haken" title="Günstiger Testlauf: Recherche und Gliederung, dann Pause"><input type="checkbox" v-model="neuStopp" /> Nach der Gliederung anhalten</label>
<div class="knoepfe"><button @click="neuStarten">Buch erzeugen</button><button class="sekundaer" @click="neuOffen = false">Abbrechen</button></div> <div class="knoepfe"><button @click="neuStarten">Buch erzeugen</button><button class="sekundaer" @click="neuOffen = false">Abbrechen</button></div>
</div> </div>
@@ -67,6 +69,7 @@ function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise:
<b>Thema</b><span>{{ state.thema }}</span> <b>Thema</b><span>{{ state.thema }}</span>
<b>Spezialisierung</b><span>{{ state.spezialisierung || '–' }}</span> <b>Spezialisierung</b><span>{{ state.spezialisierung || '–' }}</span>
<b>Hinweise</b><span>{{ state.hinweise || '–' }}</span> <b>Hinweise</b><span>{{ state.hinweise || '–' }}</span>
<template v-if="state.stopp_gliederung"><b>Lauf</b><span>hält nach der Gliederung an</span></template>
</div> </div>
<div v-if="state.deutung" class="deutung"> <div v-if="state.deutung" class="deutung">

View File

@@ -85,6 +85,8 @@ a.knopf { display: inline-block; text-decoration: none; line-height: normal; }
.neu-version { max-width: 760px; margin: 0 0 16px; padding: 12px 14px; background: var(--panel); border: 1px solid var(--rand); border-radius: 8px; display: grid; gap: 8px; } .neu-version { max-width: 760px; margin: 0 0 16px; padding: 12px 14px; background: var(--panel); border: 1px solid var(--rand); border-radius: 8px; display: grid; gap: 8px; }
.neu-version .titel { font-weight: 600; } .neu-version .titel { font-weight: 600; }
.neu-version .knoepfe { display: flex; gap: 8px; } .neu-version .knoepfe { display: flex; gap: 8px; }
.haken { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--dim); cursor: pointer; }
.haken input { accent-color: var(--akzent); }
.deutung { max-width: 760px; margin: 0 0 16px; padding: 12px 14px; background: var(--panel); border: 1px solid var(--rand); border-radius: 8px; } .deutung { max-width: 760px; margin: 0 0 16px; padding: 12px 14px; background: var(--panel); border: 1px solid var(--rand); border-radius: 8px; }
.deutung .titel { font-weight: 600; font-size: 15px; } .deutung .titel { font-weight: 600; font-size: 15px; }
.deutung .sub { color: var(--dim); font-size: 12.5px; } .deutung .sub { color: var(--dim); font-size: 12.5px; }