diff --git a/backend/generator.py b/backend/generator.py index d00d134..f85e3f9 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -360,11 +360,11 @@ FORMAT-SPEZIFIKATION: REFERENZ-BEISPIEL: {reference} -Schlage bis zu 20 Bausteine vor. Antworte AUSSCHLIESSLICH mit einem JSON-Array. Jedes Element hat: +Schlage 8 Bausteine vor. Antworte AUSSCHLIESSLICH mit einem JSON-Array. Jedes Element hat: - "title" - "description" - "purpose" -- "examples": Array mit 4 Objekten {{"label": "...", "code": "..."}} +- "examples": Array mit 1 Objekt {{"label": "...", "code": "..."}} Orientiere dich an der Spezifikation und Referenz. NUR das JSON-Array, kein weiterer Text. """ @@ -383,7 +383,7 @@ REFERENZ-BEISPIEL: {reference} Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "description", "purpose", "examples". -"examples" ist ein Array mit 4 Objekten {{"label": "...", "code": "..."}}. +"examples" ist ein Array mit 1 Objekt {{"label": "...", "code": "..."}}. Orientiere dich an der Spezifikation und Referenz. Kein weiterer Text, nur das JSON. """ @@ -409,7 +409,7 @@ async def generate_suggestions(topic: str, html_paths: list[Path]) -> None: now = datetime.now(timezone.utc).isoformat() suggestions = [] - for item in items[:20]: + for item in items[:8]: suggestions.append({ "id": str(uuid.uuid4()), "topic": topic, @@ -450,3 +450,49 @@ async def generate_baustein_detail(baustein_id: str, topic: str, title: str) -> ) except Exception: pass + + +def _build_baustein_rework_prompt(topic: str, title: str, current: dict, instructions: str) -> str: + current_json = json.dumps({ + "title": title, + "description": current.get("description", ""), + "purpose": current.get("purpose", ""), + "examples": current.get("examples", []), + }, ensure_ascii=False, indent=2) + + return f"""Überarbeite den Baustein "{title}" zum Thema "{topic}" gemäß den Anweisungen. + +AKTUELLER STAND: +{current_json} + +ANWEISUNGEN VOM NUTZER: +{instructions} + +Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "description", "purpose", "examples". +"examples" ist ein Array mit Objekten {{"label": "...", "code": "..."}}. +Kein weiterer Text, nur das JSON. +""" + + +async def rework_baustein(baustein_id: str, topic: str, title: str, current: dict, instructions: str) -> None: + try: + prompt = _build_baustein_rework_prompt(topic, title, current, instructions) + returncode, stdout, stderr = await _run_claude("baustein-" + baustein_id, prompt, 60, tools=None) + + if returncode != 0: + return + + data = _parse_json(stdout) + if not isinstance(data, dict): + return + + now = datetime.now(timezone.utc).isoformat() + await update_baustein( + baustein_id, + description=data.get("description", ""), + purpose=data.get("purpose", ""), + example=json.dumps(data.get("examples", []), ensure_ascii=False), + updated_at=now, + ) + except Exception: + pass diff --git a/backend/models.py b/backend/models.py index 48feaf0..a7bbde7 100644 --- a/backend/models.py +++ b/backend/models.py @@ -37,6 +37,10 @@ class BausteinCreateRequest(BaseModel): title: str = Field(min_length=1, max_length=200) +class BausteinReworkRequest(BaseModel): + instructions: str = Field(min_length=1, max_length=2000) + + class BausteinResponse(BaseModel): id: str topic: str diff --git a/backend/routes.py b/backend/routes.py index 53ce0f0..6fe99a5 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -11,10 +11,10 @@ from database import ( create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein, list_suggestions, get_suggestion, update_suggestion, delete_suggestion, ) -from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, is_suggestions_generating +from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, rework_baustein, is_suggestions_generating from models import ( GuideCreateRequest, GuideReworkRequest, GuideResponse, - BausteinCreateRequest, BausteinResponse, SuggestionResponse, + BausteinCreateRequest, BausteinReworkRequest, BausteinResponse, SuggestionResponse, ) from paths import final_paths @@ -148,6 +148,25 @@ async def remove_baustein(baustein_id: str): return {"ok": True} +@router.post("/bausteine/{baustein_id}/rework") +async def rework_baustein_route(baustein_id: str, req: BausteinReworkRequest): + b = await get_baustein(baustein_id) + if b is None: + raise HTTPException(404, "Baustein nicht gefunden") + import json + try: + examples = json.loads(b.get("example") or "[]") + except Exception: + examples = [] + current = { + "description": b.get("description", ""), + "purpose": b.get("purpose", ""), + "examples": examples, + } + asyncio.create_task(rework_baustein(baustein_id, b["topic"], b["title"], current, req.instructions.strip())) + return {"ok": True} + + # --- Baustein Suggestions --- @router.get("/bausteine/suggestions", response_model=list[SuggestionResponse]) diff --git a/frontend/src/api.js b/frontend/src/api.js index a05674c..4ffda61 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -62,6 +62,14 @@ export async function deleteBaustein(id) { await fetch(`${BASE}/bausteine/${id}`, { method: 'DELETE' }) } +export async function reworkBaustein(id, instructions) { + await fetch(`${BASE}/bausteine/${id}/rework`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ instructions }), + }) +} + export async function fetchSuggestions(topic) { const res = await fetch(`${BASE}/bausteine/suggestions?topic=${encodeURIComponent(topic)}`) return res.json() diff --git a/frontend/src/components/BausteineView.vue b/frontend/src/components/BausteineView.vue index d7cb76e..ee05020 100644 --- a/frontend/src/components/BausteineView.vue +++ b/frontend/src/components/BausteineView.vue @@ -4,6 +4,7 @@ import { fetchBausteine, createBaustein, deleteBaustein, + reworkBaustein, fetchSuggestions, generateSuggestions, fetchSuggestionsStatus, @@ -19,7 +20,11 @@ const bausteine = ref([]) const suggestions = ref([]) const suggestionsLoading = ref(false) const newTitle = ref('') +const reworkInputs = ref({}) +const reworkingIds = ref(new Set()) +const reworkingSnapshots = new Map() let pollTimer = null +let bausteinPollTimer = null const pendingSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'pending')) const ignoredSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'ignored')) @@ -44,10 +49,6 @@ async function checkAndGenerate() { if (status.generating) { suggestionsLoading.value = true startPolling() - } else if (suggestions.value.length === 0) { - suggestionsLoading.value = true - await generateSuggestions(props.topic) - startPolling() } } @@ -88,6 +89,37 @@ async function handleRegenerate() { startPolling() } +async function handleRework(b) { + const instructions = (reworkInputs.value[b.id] || '').trim() + if (!instructions) return + reworkingSnapshots.set(b.id, b.updated_at) + reworkingIds.value = new Set([...reworkingIds.value, b.id]) + reworkInputs.value[b.id] = '' + await reworkBaustein(b.id, instructions) + startBausteinPolling() +} + +function startBausteinPolling() { + if (bausteinPollTimer) return + bausteinPollTimer = setInterval(async () => { + const fresh = await fetchBausteine(props.topic) + bausteine.value = fresh + const remaining = new Set(reworkingIds.value) + for (const id of reworkingIds.value) { + const b = fresh.find((x) => x.id === id) + if (!b || b.updated_at !== reworkingSnapshots.get(id)) { + remaining.delete(id) + reworkingSnapshots.delete(id) + } + } + reworkingIds.value = remaining + if (remaining.size === 0) { + clearInterval(bausteinPollTimer) + bausteinPollTimer = null + } + }, 3000) +} + function startPolling() { stopPolling() pollTimer = setInterval(async () => { @@ -105,6 +137,10 @@ function stopPolling() { clearInterval(pollTimer) pollTimer = null } + if (bausteinPollTimer) { + clearInterval(bausteinPollTimer) + bausteinPollTimer = null + } } async function init() { @@ -147,23 +183,39 @@ onUnmounted(stopPolling)
{{ ex.code }}
+
Wird generiert…
+Wird überarbeitet…
+Wird generiert…
+{{ s.description }}
@@ -171,7 +223,7 @@ onUnmounted(stopPolling){{ ex.code }}
+