Hinweise und Spezialisierung je Version: im Lauf sichtbar, bei „Neue Version“ änderbar

Eingaben liegen jetzt in vN/state.json; alte Versionen ohne eigene
Eingaben nehmen die des Themas. „Neue Version“ öffnet ein Formular mit
den bisherigen Werten, Thema bleibt fest. Zurücksetzen behält die Eingaben.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
team3
2026-10-12 15:29:34 +02:00
parent 7538812cfd
commit 97e465181a
8 changed files with 70 additions and 20 deletions

6
API.md
View File

@@ -18,7 +18,7 @@ Ohne `v2` gilt die neueste Version.
– `hinweise`: wie das Thema zu verstehen ist (fügt hinzu); `spezialisierung`: Grenze des Umfangs, nur was dafür gebraucht wird (nimmt weg)
- `GET /api/themen/{slug}` → Thema wie oben
- `DELETE /api/themen/{slug}` – ganzes Thema
- `POST /api/themen/{slug}/versionen` → `{"v": N}` – nächste Version, gleiche Eingaben, ohne Start
- `POST /api/themen/{slug}/versionen` `{hinweise?, spezialisierung?}` → `{"v": N}` – nächste Version ohne Start; fehlende Felder übernimmt sie von der neuesten 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}/pause` – bricht laufende Agenten ab; Neustart mit `start`
@@ -38,7 +38,7 @@ Alte Zustände ohne diese Stufe bekommen sie beim Laden nachgetragen.
```json
{
"slug": "godot-4", "name": "Godot 4", "version": 1, "titel": "Godot 4", "hinweise": "…", "spezialisierung": "",
"slug": "godot-4", "name": "Godot 4", "version": 1, "titel": "Godot 4", "hinweise": "…", "spezialisierung": "", // Eingaben dieser Version
"status": "laeuft", "grund": "", "stufe": "schreiben", "angelegt": "2026-09-28T10:00:00Z",
"deutung": {"titel": "Godot 4", "untertitel": "…", "deutung": "…", "buchtyp": "werkzeug|wissen", "zielgruppe": "…"},
"stufen": [
@@ -94,7 +94,7 @@ Solange ein Lauf läuft, enthält `buch` die bereits geschriebenen Kapitel (Stat
storage/<Thema>/
thema.json {name, slug, titel, hinweise, spezialisierung, angelegt}
v1/, v2/, … je Version:
state.json Zustand wie oben (ohne Buch)
state.json Zustand wie oben (ohne Buch), mit hinweise und spezialisierung der Version
deutung.json vom Recherche-Agenten
position.json Leseposition
quellen/ QUELLEN.md + geklonte/geladene Quellen

View File

@@ -32,6 +32,11 @@ class ThemaNeu(BaseModel):
spezialisierung: str = ""
class VersionNeu(BaseModel):
hinweise: str | None = None # None: wie die bisherige Version
spezialisierung: str | None = None
class Position(BaseModel):
abschnitt: str | None = None
lesemodus: str | int | None = None # neu: key des Bildschirms, alt: Index
@@ -104,9 +109,10 @@ async def thema_loeschen(slug: str):
@app.post("/api/themen/{slug}/versionen")
async def version_anlegen(slug: str):
"""Neue Version: gleiche Eingaben, frischer Lauf, ohne Wissen über die Vorgänger."""
key = state.version_anlegen(_thema(slug)["name"])
async def version_anlegen(slug: str, body: VersionNeu | None = None):
"""Neue Version: frischer Lauf ohne Wissen über die Vorgänger; Hinweise und Spezialisierung wahlweise geändert."""
b = body or VersionNeu()
key = state.version_anlegen(_thema(slug)["name"], b.hinweise, b.spezialisierung)
return {"v": int(key.rsplit("/v", 1)[1])}
@@ -115,7 +121,8 @@ async def zustand(slug: str, n: int):
key = _key(slug, n)
t = _thema(slug)
st = state.zustand(key)
return {"slug": slug, "name": t["name"], "version": n, "titel": t["titel"], "hinweise": t.get("hinweise", ""), "spezialisierung": t.get("spezialisierung", ""), **st,
h, sp = state.eingaben(key)
return {"slug": slug, "name": t["name"], "version": n, "titel": t["titel"], **st, "hinweise": h, "spezialisierung": sp,
"deutung": st.get("deutung") or state.deutung(key), "ereignisse": st["ereignisse"][-100:]}

View File

@@ -73,8 +73,9 @@ def _offen(name: str) -> None:
def _thema(name: str) -> tuple[str, str, str]:
"""Titel, Hinweise, Spezialisierung – die Eingaben gelten je Version."""
t = state.thema(name) or {}
return t.get("titel") or name, t.get("hinweise", ""), t.get("spezialisierung", "")
return (t.get("titel") or name, *state.eingaben(name))
def _kapitel(name: str) -> list[dict]:

View File

@@ -55,16 +55,34 @@ def name_von_slug(sl: str) -> str | None:
return next((t["name"] for t in themen() if t.get("slug") == sl), None)
def version_anlegen(name: str) -> str:
n = (versionen(name) or [0])[-1] + 1
key = f"{name}/v{n}"
def version_anlegen(name: str, hinweise: str | None = None, spezialisierung: str | None = None) -> str:
"""Neue Version; ohne Angabe übernimmt sie Hinweise und Spezialisierung der neuesten Version (bzw. des Themas)."""
vs = versionen(name)
alt_h, alt_s = eingaben(f"{name}/v{vs[-1]}") if vs else _eingaben_thema(name)
key = f"{name}/v{(vs or [0])[-1] + 1}"
v = verzeichnis(key)
for u in ("quellen", "buch", "berichte", "arbeit"):
(v / u).mkdir(parents=True)
_schreiben(v / "state.json", leerer_zustand())
st = leerer_zustand()
st["hinweise"] = (alt_h if hinweise is None else hinweise).strip()
st["spezialisierung"] = (alt_s if spezialisierung is None else spezialisierung).strip()
_schreiben(v / "state.json", st)
return key
def _eingaben_thema(name: str) -> tuple[str, str]:
t = thema(name) or {}
return t.get("hinweise", ""), t.get("spezialisierung", "")
def eingaben(key: str) -> tuple[str, str]:
"""Hinweise und Spezialisierung einer Version; alte Versionen ohne eigene Eingaben nehmen die des Themas."""
st = zustand(key)
if "hinweise" in st:
return st["hinweise"], st.get("spezialisierung", "")
return _eingaben_thema(key)
def version_loeschen(key: str) -> None:
_cache.pop(key, None)
shutil.rmtree(verzeichnis(key), ignore_errors=True)
@@ -138,13 +156,14 @@ def loeschen(name: str) -> None:
def zuruecksetzen(key: str) -> None:
v = verzeichnis(key)
h, s = eingaben(key)
_cache.pop(key, None)
for u in ("quellen", "buch", "berichte", "arbeit"):
shutil.rmtree(v / u, ignore_errors=True)
(v / u).mkdir()
for f in ("deutung.json", "log.jsonl", "state.json", "position.json"):
(v / f).unlink(missing_ok=True)
_schreiben(v / "state.json", leerer_zustand())
_schreiben(v / "state.json", {**leerer_zustand(), "hinweise": h, "spezialisierung": s})
def thema(key: str) -> dict | None:

View File

@@ -113,10 +113,10 @@ async function anlegen() {
themaWaehlen(t.slug)
} catch (e) { fehler.value = String(e.message || e) }
}
// Neue Version: gleiche Eingaben, frischer Lauf; die alte Version bleibt samt Leseposition erhalten.
async function neueVersion() {
// Neue Version: frischer Lauf mit ggf. geänderten Hinweisen/Spezialisierung; die alte Version bleibt samt Leseposition erhalten.
async function neueVersion(eingaben) {
try {
const r = await api.versionAnlegen(slug.value)
const r = await api.versionAnlegen(slug.value, eingaben)
await topicsLaden()
geheZu({ v: r.v, tab: 'lauf' })
await aktion(() => api.start(slug.value, r.v))

View File

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

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { render } from '../markdown.js'
const props = defineProps({ state: Object })
@@ -36,7 +36,12 @@ const gruppen = computed(() => {
const ereignisse = computed(() => (props.state?.ereignisse || []).slice(-100).reverse())
function reset() { if (confirm('Lauf zurücksetzen? Alles außer dem Thema wird gelöscht.')) emit('reset') }
function neueVersion() { if (confirm('Neue Version erzeugen? Gleiche Eingaben, kompletter neuer Lauf. Die bisherige Version bleibt erhalten.')) emit('neue-version') }
// Neue Version: Formular mit den Eingaben dieser Version vorbelegt; Thema bleibt, Hinweise und Spezialisierung sind änderbar.
const neuOffen = ref(false)
const neuHinweise = ref('')
const neuSpezialisierung = ref('')
function neuOeffnen() { neuHinweise.value = props.state.hinweise || ''; neuSpezialisierung.value = props.state.spezialisierung || ''; neuOffen.value = true }
function neuStarten() { neuOffen.value = false; emit('neue-version', { hinweise: neuHinweise.value.trim(), spezialisierung: neuSpezialisierung.value.trim() }) }
</script>
<template>
@@ -45,7 +50,7 @@ function neueVersion() { if (confirm('Neue Version erzeugen? Gleiche Eingaben, k
<button v-if="!laeuft && !fertig" @click="emit('start')">{{ begonnen ? 'Fortsetzen' : 'Buch erzeugen' }}</button>
<button v-if="laeuft" class="sekundaer" @click="emit('pause')">Pause</button>
<button v-if="!laeuft && begonnen && !fertig" class="sekundaer" @click="reset">Zurücksetzen</button>
<button v-if="!laeuft && begonnen" class="sekundaer" title="Gleiches Thema, gleiche Hinweise und Spezialisierung, neuer Lauf" @click="neueVersion">Neue Version</button>
<button v-if="!laeuft && begonnen" class="sekundaer" title="Gleiches Thema, kompletter neuer Lauf; Hinweise und Spezialisierung änderbar" @click="neuOeffnen">Neue Version</button>
<span class="spacer"></span>
<span class="badge" :class="statusFarbe[state.status]">{{ statusText[state.status] || state.status }}<template v-if="state.stufe && laeuft"> · {{ state.stufe }}</template></span>
<span class="badge">{{ usd(state.kosten_usd) }}</span>
@@ -53,6 +58,19 @@ function neueVersion() { if (confirm('Neue Version erzeugen? Gleiche Eingaben, k
</div>
<div v-if="state.grund" class="grund">{{ state.grund }}</div>
<div v-if="neuOffen" class="neu-version">
<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="neuSpezialisierung" placeholder="Spezialisierung (optional), z. B. Kartenspiel gegen KI" title="Begrenzt den Umfang: nur, was dafür gebraucht wird" @keyup.enter="neuStarten" />
<div class="knoepfe"><button @click="neuStarten">Buch erzeugen</button><button class="sekundaer" @click="neuOffen = false">Abbrechen</button></div>
</div>
<div class="eingaben">
<span><b>Thema</b> {{ state.name }}</span>
<span><b>Hinweise</b> {{ state.hinweise || '–' }}</span>
<span><b>Spezialisierung</b> {{ state.spezialisierung || '–' }}</span>
</div>
<div v-if="state.deutung" class="deutung">
<div class="titel">{{ state.deutung.titel }}</div>
<div v-if="state.deutung.untertitel" class="sub">{{ state.deutung.untertitel }}</div>

View File

@@ -80,6 +80,11 @@ a.knopf { display: inline-block; text-decoration: none; line-height: normal; }
.lauf { padding: 18px max(22px, calc((100% - 760px) / 2)); overflow-y: auto; } /* Spalte zentriert wie in der Buchansicht */
.lauf .aktionen { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 14px; }
.lauf .grund { margin: 8px 0 14px; padding: 8px 12px; background: var(--karte); border-left: 3px solid var(--gelb); border-radius: 4px; }
.lauf .eingaben { max-width: 760px; margin: 0 0 12px; display: flex; gap: 14px; flex-wrap: wrap; font-size: 12.5px; color: var(--dim); }
.lauf .eingaben b { font-weight: 600; color: var(--text); margin-right: 4px; }
.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 .knoepfe { display: flex; gap: 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 .sub { color: var(--dim); font-size: 12.5px; }