This commit is contained in:
Team3
2026-05-28 22:38:01 +02:00
parent 4594c2e372
commit 96536498d0
7 changed files with 219 additions and 48 deletions

View File

@@ -360,11 +360,11 @@ FORMAT-SPEZIFIKATION:
REFERENZ-BEISPIEL: REFERENZ-BEISPIEL:
{reference} {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" - "title"
- "description" - "description"
- "purpose" - "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. Orientiere dich an der Spezifikation und Referenz. NUR das JSON-Array, kein weiterer Text.
""" """
@@ -383,7 +383,7 @@ REFERENZ-BEISPIEL:
{reference} {reference}
Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "description", "purpose", "examples". 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. 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() now = datetime.now(timezone.utc).isoformat()
suggestions = [] suggestions = []
for item in items[:20]: for item in items[:8]:
suggestions.append({ suggestions.append({
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"topic": topic, "topic": topic,
@@ -450,3 +450,49 @@ async def generate_baustein_detail(baustein_id: str, topic: str, title: str) ->
) )
except Exception: except Exception:
pass 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

View File

@@ -37,6 +37,10 @@ class BausteinCreateRequest(BaseModel):
title: str = Field(min_length=1, max_length=200) title: str = Field(min_length=1, max_length=200)
class BausteinReworkRequest(BaseModel):
instructions: str = Field(min_length=1, max_length=2000)
class BausteinResponse(BaseModel): class BausteinResponse(BaseModel):
id: str id: str
topic: str topic: str

View File

@@ -11,10 +11,10 @@ from database import (
create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein, create_baustein as db_create_baustein, list_bausteine, get_baustein, delete_baustein as db_delete_baustein,
list_suggestions, get_suggestion, update_suggestion, delete_suggestion, 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 ( from models import (
GuideCreateRequest, GuideReworkRequest, GuideResponse, GuideCreateRequest, GuideReworkRequest, GuideResponse,
BausteinCreateRequest, BausteinResponse, SuggestionResponse, BausteinCreateRequest, BausteinReworkRequest, BausteinResponse, SuggestionResponse,
) )
from paths import final_paths from paths import final_paths
@@ -148,6 +148,25 @@ async def remove_baustein(baustein_id: str):
return {"ok": True} 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 --- # --- Baustein Suggestions ---
@router.get("/bausteine/suggestions", response_model=list[SuggestionResponse]) @router.get("/bausteine/suggestions", response_model=list[SuggestionResponse])

View File

@@ -62,6 +62,14 @@ export async function deleteBaustein(id) {
await fetch(`${BASE}/bausteine/${id}`, { method: 'DELETE' }) 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) { export async function fetchSuggestions(topic) {
const res = await fetch(`${BASE}/bausteine/suggestions?topic=${encodeURIComponent(topic)}`) const res = await fetch(`${BASE}/bausteine/suggestions?topic=${encodeURIComponent(topic)}`)
return res.json() return res.json()

View File

@@ -4,6 +4,7 @@ import {
fetchBausteine, fetchBausteine,
createBaustein, createBaustein,
deleteBaustein, deleteBaustein,
reworkBaustein,
fetchSuggestions, fetchSuggestions,
generateSuggestions, generateSuggestions,
fetchSuggestionsStatus, fetchSuggestionsStatus,
@@ -19,7 +20,11 @@ const bausteine = ref([])
const suggestions = ref([]) const suggestions = ref([])
const suggestionsLoading = ref(false) const suggestionsLoading = ref(false)
const newTitle = ref('') const newTitle = ref('')
const reworkInputs = ref({})
const reworkingIds = ref(new Set())
const reworkingSnapshots = new Map()
let pollTimer = null let pollTimer = null
let bausteinPollTimer = null
const pendingSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'pending')) const pendingSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'pending'))
const ignoredSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'ignored')) const ignoredSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'ignored'))
@@ -44,10 +49,6 @@ async function checkAndGenerate() {
if (status.generating) { if (status.generating) {
suggestionsLoading.value = true suggestionsLoading.value = true
startPolling() startPolling()
} else if (suggestions.value.length === 0) {
suggestionsLoading.value = true
await generateSuggestions(props.topic)
startPolling()
} }
} }
@@ -88,6 +89,37 @@ async function handleRegenerate() {
startPolling() 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() { function startPolling() {
stopPolling() stopPolling()
pollTimer = setInterval(async () => { pollTimer = setInterval(async () => {
@@ -105,6 +137,10 @@ function stopPolling() {
clearInterval(pollTimer) clearInterval(pollTimer)
pollTimer = null pollTimer = null
} }
if (bausteinPollTimer) {
clearInterval(bausteinPollTimer)
bausteinPollTimer = null
}
} }
async function init() { async function init() {
@@ -147,23 +183,39 @@ onUnmounted(stopPolling)
<div v-if="b.example" class="examples"> <div v-if="b.example" class="examples">
<div v-for="(ex, i) in parseExamples(b.example)" :key="i" class="code-block"> <div v-for="(ex, i) in parseExamples(b.example)" :key="i" class="code-block">
<span v-if="ex.label" class="code-label">{{ ex.label }}</span> <span v-if="ex.label" class="code-label">{{ ex.label }}</span>
<pre>{{ ex.code }}</pre> <pre v-html="ex.code"></pre>
</div> </div>
</div> </div>
<p v-if="!b.description && !b.purpose" class="loading-text">Wird generiert</p> <p v-if="reworkingIds.has(b.id)" class="loading-text">Wird überarbeitet</p>
<p v-else-if="!b.description && !b.purpose" class="loading-text">Wird generiert</p>
<div class="rework-row">
<input
v-model="reworkInputs[b.id]"
placeholder="Anpassen…"
:disabled="reworkingIds.has(b.id)"
@keyup.enter="handleRework(b)"
/>
<button
class="rework-btn"
:disabled="!reworkInputs[b.id]?.trim() || reworkingIds.has(b.id)"
@click="handleRework(b)"
title="Überarbeiten"
></button>
</div>
</div> </div>
</div> </div>
<hr v-if="pendingSuggestions.length || suggestionsLoading" class="divider" /> <hr class="divider" />
<div v-if="suggestionsLoading" class="loading-indicator">Generiere Vorschläge</div> <div class="suggestions-section">
<div v-if="pendingSuggestions.length" class="suggestions-section">
<div class="section-header"> <div class="section-header">
<h3>Vorschläge</h3> <h3>Vorschläge</h3>
<button class="regenerate-btn" @click="handleRegenerate" :disabled="suggestionsLoading">Neu generieren</button> <button class="regenerate-btn" @click="handleRegenerate" :disabled="suggestionsLoading">
{{ suggestionsLoading ? 'Generiere…' : 'Generieren' }}
</button>
</div> </div>
<div class="suggestions-grid"> <div v-if="suggestionsLoading" class="loading-indicator">Generiere Vorschläge</div>
<div v-if="pendingSuggestions.length" class="suggestions-grid">
<div v-for="s in pendingSuggestions" :key="s.id" class="card suggestion-card"> <div v-for="s in pendingSuggestions" :key="s.id" class="card suggestion-card">
<h3>{{ s.title }}</h3> <h3>{{ s.title }}</h3>
<p v-if="s.description" class="desc">{{ s.description }}</p> <p v-if="s.description" class="desc">{{ s.description }}</p>
@@ -171,7 +223,7 @@ onUnmounted(stopPolling)
<div v-if="s.example" class="examples"> <div v-if="s.example" class="examples">
<div v-for="(ex, i) in parseExamples(s.example)" :key="i" class="code-block"> <div v-for="(ex, i) in parseExamples(s.example)" :key="i" class="code-block">
<span v-if="ex.label" class="code-label">{{ ex.label }}</span> <span v-if="ex.label" class="code-label">{{ ex.label }}</span>
<pre>{{ ex.code }}</pre> <pre v-html="ex.code"></pre>
</div> </div>
</div> </div>
<div class="suggestion-actions"> <div class="suggestion-actions">
@@ -205,7 +257,7 @@ onUnmounted(stopPolling)
.bausteine-grid, .bausteine-grid,
.suggestions-grid { .suggestions-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 1rem; gap: 1rem;
} }
@@ -328,12 +380,67 @@ onUnmounted(stopPolling)
display: block; 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 { .loading-text {
font-size: 0.8rem; font-size: 0.8rem;
color: #9ca3af; color: #9ca3af;
font-style: italic; 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 { .divider {
border: none; border: none;
border-top: 1px solid #e2e5e9; border-top: 1px solid #e2e5e9;

View File

@@ -20,7 +20,7 @@ STRUKTUR — STRIKT NUR DIESE 4 ELEMENTE
1. Titel (h1, fett, ohne Label, ohne Logo) 1. Titel (h1, fett, ohne Label, ohne Logo)
2. Beschreibung (ein knapper Satz) 2. Beschreibung (ein knapper Satz)
3. Zweck (kursiv, grau, 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. 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) - Beschreibung: 1 Satz, was es macht (mechanisch, neutral)
- Zweck: 1 Satz, wofür man es nutzt (Anwendungsfall) - Zweck: 1 Satz, wofür man es nutzt (Anwendungsfall)
- Beide Sätze knapp wie möglich, jedes überflüssige Wort raus - 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 LAYOUT-DETAILS
- Header und Body keine separaten Sektionen, alles in einer Card - Header und Body keine separaten Sektionen, alles in einer Card
- Titel zuerst, darunter direkt Beschreibung, darunter Zweck (mit margin-bottom) - Titel zuerst, darunter direkt Beschreibung, darunter Zweck (mit margin-bottom)
- Margin-bottom Titel: 14px
- Margin-bottom Beschreibung: 6px (eng zum Zweck) - Margin-bottom Beschreibung: 6px (eng zum Zweck)
- Margin-bottom Zweck: 22px (Abstand zu Code) - 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-Block padding: 14px 16px, border-radius 8px
CODE-BEISPIELE CODE-BEISPIEL
- So viele wie nötig, so wenige wie möglich - GENAU EIN Beispiel pro Card
- Untereinander gestapelt (grid-template-columns: 1fr) - Das relevanteste, typischste, häufigste — nicht Edge-Cases
- Jedes Beispiel zeigt eine sinnvolle Variante / Use-Case
- Sehr kurz: ideal 3-6 Zeilen, max 8 Zeilen - 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.) - 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 VERMEIDEN
- Lange Erklärungstexte - Lange Erklärungstexte
@@ -83,22 +80,24 @@ VERMEIDEN
- Tabellen, Listen, Aufzählungen - Tabellen, Listen, Aufzählungen
- Icons, Emojis, Symbole - Icons, Emojis, Symbole
- Komplexe Code-Beispiele (über 8 Zeilen) - Komplexe Code-Beispiele (über 8 Zeilen)
- Mehrere Beispiele (Varianten, Alternativen)
THEMENSPEZIFISCHE ANPASSUNGEN THEMENSPEZIFISCHE ANPASSUNGEN
- Bei anderen Sprachen: Syntax-Highlighting-Klassen anpassen - Bei anderen Sprachen: Syntax-Highlighting-Klassen anpassen
- Titel-Größe und Spacing bleiben gleich - Titel-Größe und Spacing bleiben gleich
- Card-Layout bleibt gleich - Card-Layout bleibt gleich
- Inhalte (Beschreibung, Zweck, Beispiele) sprachspezifisch - Inhalte (Beschreibung, Zweck, Beispiel) sprachspezifisch
GENERIERUNG MIT FEEDBACK-LOOP GENERIERUNG MIT FEEDBACK-LOOP
1. HTML schreiben 1. HTML schreiben
2. In Browser anzeigen (Playwright-Screenshot oder direkt) 2. In Browser anzeigen (Playwright-Screenshot oder direkt)
3. Prüfen: 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? - Beschreibung und Zweck unter 15 Wörtern?
- Code-Beispiele unter 8 Zeilen? - Code-Beispiel unter 8 Zeilen?
- Labels über Code-Blöcken kurz und prägnant? - Label über Code-Block kurz und prägnant?
- Card kompakt, kein leerer Raum? - Card kompakt, kein leerer Raum?
- Ist das gewählte Beispiel wirklich das typischste?
4. Wenn etwas zu viel: weglassen, nicht hinzufügen 4. Wenn etwas zu viel: weglassen, nicht hinzufügen
5. Bei jeder Iteration prüfen: lässt sich noch was weglassen? 5. Bei jeder Iteration prüfen: lässt sich noch was weglassen?
``` ```

View File

@@ -98,18 +98,6 @@ h1 {
<span class="k">echo</span> <span class="v">$i</span>; <span class="k">echo</span> <span class="v">$i</span>;
}</div> }</div>
<div class="code-block"><span class="label">Rückwärts zählen</span><span class="k">for</span> (<span class="v">$i</span> = <span class="n">10</span>; <span class="v">$i</span> &gt; <span class="n">0</span>; <span class="v">$i</span>--) {
<span class="k">echo</span> <span class="v">$i</span>;
}</div>
<div class="code-block"><span class="label">In 2er-Schritten</span><span class="k">for</span> (<span class="v">$i</span> = <span class="n">0</span>; <span class="v">$i</span> &lt;= <span class="n">20</span>; <span class="v">$i</span> += <span class="n">2</span>) {
<span class="k">echo</span> <span class="v">$i</span>;
}</div>
<div class="code-block"><span class="label">Mit Array-Index</span><span class="k">for</span> (<span class="v">$i</span> = <span class="n">0</span>; <span class="v">$i</span> &lt; <span class="n">3</span>; <span class="v">$i</span>++) {
<span class="k">echo</span> <span class="v">$arr</span>[<span class="v">$i</span>];
}</div>
</div> </div>
</div> </div>