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.label }} -
{{ ex.code }}
+

           
-

Wird generiert…

+

Wird überarbeitet…

+

Wird generiert…

+
+ + +
-
+
-
Generiere Vorschläge…
- -
+

Vorschläge

- +
-
+
Generiere Vorschläge…
+

{{ s.title }}

{{ s.description }}

@@ -171,7 +223,7 @@ onUnmounted(stopPolling)
{{ ex.label }} -
{{ ex.code }}
+

             
@@ -205,7 +257,7 @@ onUnmounted(stopPolling) .bausteine-grid, .suggestions-grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); gap: 1rem; } @@ -328,12 +380,67 @@ onUnmounted(stopPolling) display: block; } +.code-block :deep(.k) { color: #ff79c6; } +.code-block :deep(.v) { color: #ffb86c; } +.code-block :deep(.s) { color: #f1c40f; } +.code-block :deep(.f) { color: #50fa7b; } +.code-block :deep(.t) { color: #8be9fd; } +.code-block :deep(.c) { color: #6b8aae; font-style: italic; } +.code-block :deep(.n) { color: #ffb86c; } +.code-block :deep(.p) { color: #e6e6e6; } + .loading-text { font-size: 0.8rem; color: #9ca3af; font-style: italic; } +.rework-row { + display: flex; + gap: 6px; + margin-top: auto; + padding-top: 8px; +} + +.rework-row input { + flex: 1; + padding: 5px 8px; + border: 1px solid #d8dde3; + border-radius: 4px; + font-size: 0.8rem; + outline: none; +} + +.rework-row input:focus { + border-color: #6366f1; +} + +.rework-row input:disabled { + background: #f3f4f6; + cursor: not-allowed; +} + +.rework-btn { + width: 28px; + height: 28px; + border: 1px solid #d8dde3; + border-radius: 4px; + background: #fff; + color: #4b5563; + font-size: 0.9rem; + cursor: pointer; +} + +.rework-btn:hover:not(:disabled) { + border-color: #6366f1; + color: #6366f1; +} + +.rework-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + .divider { border: none; border-top: 1px solid #e2e5e9; diff --git a/templates/Format/Baustein.md b/templates/Format/Baustein.md index c6accff..2c7fde3 100644 --- a/templates/Format/Baustein.md +++ b/templates/Format/Baustein.md @@ -20,7 +20,7 @@ STRUKTUR — STRIKT NUR DIESE 4 ELEMENTE 1. Titel (h1, fett, ohne Label, ohne Logo) 2. Beschreibung (ein knapper Satz) 3. Zweck (kursiv, grau, ein knapper Satz) -4. Code-Beispiele (untereinander, mit Mini-Label oben) +4. Code-Beispiel (das eine relevanteste, mit Mini-Label oben) KEIN Logo, KEIN "Baustein"-Label, KEIN Sprach-Tag, KEINE Sektion-Überschriften ("Beschreibung", "Zweck", "Relevante Beispiele"), KEINE Info-Blöcke, KEINE Warn/Tip/Note-Boxen, KEINE Meta-Informationen, KEINE Trennlinien zwischen Sektionen. @@ -53,27 +53,24 @@ INHALTLICHE PRINZIPIEN - Beschreibung: 1 Satz, was es macht (mechanisch, neutral) - Zweck: 1 Satz, wofür man es nutzt (Anwendungsfall) - Beide Sätze knapp wie möglich, jedes überflüssige Wort raus -- Pro Beispiel ein knappes Label oben (2-4 Wörter, uppercase) +- Genau EIN Code-Beispiel: das relevanteste / häufigste / typischste +- Beispiel zeigt den Standard-Use-Case, nicht Edge-Cases +- Knappes Label über dem Beispiel (2-4 Wörter, uppercase) LAYOUT-DETAILS - Header und Body keine separaten Sektionen, alles in einer Card - Titel zuerst, darunter direkt Beschreibung, darunter Zweck (mit margin-bottom) +- Margin-bottom Titel: 14px - Margin-bottom Beschreibung: 6px (eng zum Zweck) - Margin-bottom Zweck: 22px (Abstand zu Code) -- Margin-bottom Titel: 14px -- Code-Blöcke untereinander, gap 14px - Code-Block padding: 14px 16px, border-radius 8px -CODE-BEISPIELE -- So viele wie nötig, so wenige wie möglich -- Untereinander gestapelt (grid-template-columns: 1fr) -- Jedes Beispiel zeigt eine sinnvolle Variante / Use-Case +CODE-BEISPIEL +- GENAU EIN Beispiel pro Card +- Das relevanteste, typischste, häufigste — nicht Edge-Cases - Sehr kurz: ideal 3-6 Zeilen, max 8 Zeilen -- Mit kurzem Label was die Variante zeigt +- Mit kurzem Label oben (2-4 Wörter) - Syntax-Highlighting durch span-Klassen (.k, .v, .s, etc.) -- Triviale Bausteine: 1-2 Beispiele -- Komplexere Bausteine: 4-6 Beispiele -- Anzahl ergibt sich aus dem Inhalt, nicht aus Vorgabe VERMEIDEN - Lange Erklärungstexte @@ -83,22 +80,24 @@ VERMEIDEN - Tabellen, Listen, Aufzählungen - Icons, Emojis, Symbole - Komplexe Code-Beispiele (über 8 Zeilen) +- Mehrere Beispiele (Varianten, Alternativen) THEMENSPEZIFISCHE ANPASSUNGEN - Bei anderen Sprachen: Syntax-Highlighting-Klassen anpassen - Titel-Größe und Spacing bleiben gleich - Card-Layout bleibt gleich -- Inhalte (Beschreibung, Zweck, Beispiele) sprachspezifisch +- Inhalte (Beschreibung, Zweck, Beispiel) sprachspezifisch GENERIERUNG MIT FEEDBACK-LOOP 1. HTML schreiben 2. In Browser anzeigen (Playwright-Screenshot oder direkt) 3. Prüfen: - - Wirklich nur 4 Elemente (Titel, Beschreibung, Zweck, Beispiele)? + - Wirklich nur 4 Elemente (Titel, Beschreibung, Zweck, Beispiel)? - Beschreibung und Zweck unter 15 Wörtern? - - Code-Beispiele unter 8 Zeilen? - - Labels über Code-Blöcken kurz und prägnant? + - Code-Beispiel unter 8 Zeilen? + - Label über Code-Block kurz und prägnant? - Card kompakt, kein leerer Raum? + - Ist das gewählte Beispiel wirklich das typischste? 4. Wenn etwas zu viel: weglassen, nicht hinzufügen 5. Bei jeder Iteration prüfen: lässt sich noch was weglassen? ``` \ No newline at end of file diff --git a/templates/Referenz/Baustein.md b/templates/Referenz/Baustein.md index b4a7107..cc10c61 100644 --- a/templates/Referenz/Baustein.md +++ b/templates/Referenz/Baustein.md @@ -98,18 +98,6 @@ h1 { echo $i; }
-
Rückwärts zählenfor ($i = 10; $i > 0; $i--) { - echo $i; -}
- -
In 2er-Schrittenfor ($i = 0; $i <= 20; $i += 2) { - echo $i; -}
- -
Mit Array-Indexfor ($i = 0; $i < 3; $i++) { - echo $arr[$i]; -}
-