diff --git a/backend/database.py b/backend/database.py index 2c89416..a13cd60 100644 --- a/backend/database.py +++ b/backend/database.py @@ -1,7 +1,7 @@ import aiosqlite from config import DB_PATH -CREATE_TABLE = """ +CREATE_GUIDES = """ CREATE TABLE IF NOT EXISTS guides ( id TEXT PRIMARY KEY, topic TEXT NOT NULL, @@ -17,6 +17,32 @@ CREATE TABLE IF NOT EXISTS guides ( ) """ +CREATE_BAUSTEINE = """ +CREATE TABLE IF NOT EXISTS bausteine ( + id TEXT PRIMARY KEY, + topic TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + purpose TEXT NOT NULL DEFAULT '', + example TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +) +""" + +CREATE_SUGGESTIONS = """ +CREATE TABLE IF NOT EXISTS baustein_suggestions ( + id TEXT PRIMARY KEY, + topic TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + purpose TEXT NOT NULL DEFAULT '', + example TEXT NOT NULL DEFAULT '', + status TEXT NOT NULL DEFAULT 'pending', + created_at TEXT NOT NULL +) +""" + _db: aiosqlite.Connection | None = None @@ -30,7 +56,9 @@ async def get_db() -> aiosqlite.Connection: async def init_db(): db = await get_db() - await db.execute(CREATE_TABLE) + await db.execute(CREATE_GUIDES) + await db.execute(CREATE_BAUSTEINE) + await db.execute(CREATE_SUGGESTIONS) cursor = await db.execute("PRAGMA table_info(guides)") columns = {row[1] for row in await cursor.fetchall()} if "instructions" not in columns: @@ -94,3 +122,102 @@ async def delete_guide(guide_id: str) -> bool: cursor = await db.execute("DELETE FROM guides WHERE id = ?", (guide_id,)) await db.commit() return cursor.rowcount > 0 + + +# --- Bausteine --- + +async def create_baustein(baustein: dict) -> dict: + db = await get_db() + await db.execute( + """INSERT INTO bausteine (id, topic, title, description, purpose, example, created_at, updated_at) + VALUES (:id, :topic, :title, :description, :purpose, :example, :created_at, :updated_at)""", + baustein, + ) + await db.commit() + return baustein + + +async def list_bausteine(topic: str) -> list[dict]: + db = await get_db() + cursor = await db.execute( + "SELECT * FROM bausteine WHERE topic = ? ORDER BY created_at ASC", (topic,) + ) + rows = await cursor.fetchall() + return [_row_to_dict(row, cursor) for row in rows] + + +async def get_baustein(baustein_id: str) -> dict | None: + db = await get_db() + cursor = await db.execute("SELECT * FROM bausteine WHERE id = ?", (baustein_id,)) + row = await cursor.fetchone() + if row is None: + return None + return _row_to_dict(row, cursor) + + +async def update_baustein(baustein_id: str, **fields) -> None: + sets = ", ".join(f"{k} = :{k}" for k in fields) + fields["id"] = baustein_id + db = await get_db() + await db.execute(f"UPDATE bausteine SET {sets} WHERE id = :id", fields) + await db.commit() + + +async def delete_baustein(baustein_id: str) -> bool: + db = await get_db() + cursor = await db.execute("DELETE FROM bausteine WHERE id = ?", (baustein_id,)) + await db.commit() + return cursor.rowcount > 0 + + +# --- Baustein Suggestions --- + +async def create_suggestions(suggestions: list[dict]) -> None: + db = await get_db() + await db.executemany( + """INSERT INTO baustein_suggestions (id, topic, title, description, purpose, example, status, created_at) + VALUES (:id, :topic, :title, :description, :purpose, :example, :status, :created_at)""", + suggestions, + ) + await db.commit() + + +async def list_suggestions(topic: str) -> list[dict]: + db = await get_db() + cursor = await db.execute( + "SELECT * FROM baustein_suggestions WHERE topic = ? ORDER BY created_at ASC", (topic,) + ) + rows = await cursor.fetchall() + return [_row_to_dict(row, cursor) for row in rows] + + +async def get_suggestion(suggestion_id: str) -> dict | None: + db = await get_db() + cursor = await db.execute("SELECT * FROM baustein_suggestions WHERE id = ?", (suggestion_id,)) + row = await cursor.fetchone() + if row is None: + return None + return _row_to_dict(row, cursor) + + +async def update_suggestion(suggestion_id: str, **fields) -> None: + sets = ", ".join(f"{k} = :{k}" for k in fields) + fields["id"] = suggestion_id + db = await get_db() + await db.execute(f"UPDATE baustein_suggestions SET {sets} WHERE id = :id", fields) + await db.commit() + + +async def delete_suggestion(suggestion_id: str) -> bool: + db = await get_db() + cursor = await db.execute("DELETE FROM baustein_suggestions WHERE id = ?", (suggestion_id,)) + await db.commit() + return cursor.rowcount > 0 + + +async def delete_pending_suggestions(topic: str) -> None: + db = await get_db() + await db.execute( + "DELETE FROM baustein_suggestions WHERE topic = ? AND status = 'pending'", (topic,) + ) + await db.commit() diff --git a/backend/generator.py b/backend/generator.py index 0afec01..7660a17 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -1,6 +1,9 @@ import asyncio +import json +import re import subprocess import tempfile +import uuid from datetime import datetime, timezone from pathlib import Path @@ -13,7 +16,14 @@ from config import ( REVIEW_TIMEOUTS, STORAGE_DIR, ) -from database import update_guide +from database import ( + update_guide, + create_baustein, + create_suggestions, + delete_pending_suggestions, + list_bausteine, + update_baustein, +) _semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS) _active_processes: dict[str, asyncio.subprocess.Process] = {} @@ -35,12 +45,13 @@ async def _set_progress(guide_id: str, progress: str) -> None: await update_guide(guide_id, progress=progress, updated_at=now) -async def _run_claude(guide_id: str, prompt: str, timeout: int) -> tuple[int, str, str]: +async def _run_claude(guide_id: str, prompt: str, timeout: int, tools: str | None = "Write,Bash,Read,WebSearch,WebFetch") -> tuple[int, str, str]: + cmd = [CLAUDE_CLI, "-p"] + if tools: + cmd += ["--allowedTools", tools] + cmd += ["--dangerously-skip-permissions"] process = await asyncio.create_subprocess_exec( - CLAUDE_CLI, - "-p", - "--allowedTools", "Write,Bash,Read,WebSearch,WebFetch", - "--dangerously-skip-permissions", + *cmd, stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, @@ -397,3 +408,136 @@ async def rework_guide(guide_id: str, topic: str, format_name: str, instructions async def _fail(guide_id: str, msg: str) -> None: now = datetime.now(timezone.utc).isoformat() await update_guide(guide_id, status="error", progress=None, error_msg=msg, updated_at=now) + + +# --- Bausteine --- + +_suggestions_generating: set[str] = set() + + +def is_suggestions_generating(topic: str) -> bool: + return topic in _suggestions_generating + + +def _parse_json(text: str): + text = text.strip() + text = re.sub(r"^```(?:json)?\s*", "", text) + text = re.sub(r"\s*```$", "", text) + return json.loads(text) + + +def _build_suggestions_prompt(topic: str, html_paths: list[Path], existing_titles: list[str]) -> str: + spec = (TEMPLATES_DIR / "Format" / "Baustein.md").read_text(encoding="utf-8") + reference = (TEMPLATES_DIR / "Referenz" / "Baustein.md").read_text(encoding="utf-8") + existing_list = "\n".join(f"- {t}" for t in existing_titles) if existing_titles else "(keine)" + + if html_paths: + read_instructions = "\n".join(f"- Lies: {p}" for p in html_paths) + guides_section = f"""SCHRITT 1 — Guides lesen: +{read_instructions} + +""" + else: + guides_section = "" + + return f"""Schlage fundamentale Bausteine (Kernkonzepte) zum Thema "{topic}" vor. + +{guides_section}Bereits vorhandene Bausteine (NICHT erneut vorschlagen): +{existing_list} + +FORMAT-SPEZIFIKATION: +{spec} + +REFERENZ-BEISPIEL: +{reference} + +Schlage bis zu 20 Bausteine vor. Antworte AUSSCHLIESSLICH mit einem JSON-Array. Jedes Element hat: +- "title" +- "description" +- "purpose" +- "examples": Array mit 4 Objekten {{"label": "...", "code": "..."}} + +Orientiere dich an der Spezifikation und Referenz. NUR das JSON-Array, kein weiterer Text. +""" + + +def _build_baustein_detail_prompt(topic: str, title: str) -> str: + spec = (TEMPLATES_DIR / "Format" / "Baustein.md").read_text(encoding="utf-8") + reference = (TEMPLATES_DIR / "Referenz" / "Baustein.md").read_text(encoding="utf-8") + + return f"""Generiere Details für den Baustein "{title}" im Kontext des Themas "{topic}". + +FORMAT-SPEZIFIKATION: +{spec} + +REFERENZ-BEISPIEL: +{reference} + +Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "description", "purpose", "examples". +"examples" ist ein Array mit 4 Objekten {{"label": "...", "code": "..."}}. +Orientiere dich an der Spezifikation und Referenz. Kein weiterer Text, nur das JSON. +""" + + +async def generate_suggestions(topic: str, html_paths: list[Path]) -> None: + _suggestions_generating.add(topic) + try: + existing = await list_bausteine(topic) + existing_titles = [b["title"] for b in existing] + + await delete_pending_suggestions(topic) + + prompt = _build_suggestions_prompt(topic, html_paths, existing_titles) + tools = "Read" if html_paths else None + returncode, stdout, stderr = await _run_claude("suggestions-" + topic, prompt, 180, tools=tools) + + if returncode != 0: + return + + items = _parse_json(stdout) + if not isinstance(items, list): + return + + now = datetime.now(timezone.utc).isoformat() + suggestions = [] + for item in items[:20]: + suggestions.append({ + "id": str(uuid.uuid4()), + "topic": topic, + "title": item.get("title", ""), + "description": item.get("description", ""), + "purpose": item.get("purpose", ""), + "example": json.dumps(item.get("examples", []), ensure_ascii=False), + "status": "pending", + "created_at": now, + }) + if suggestions: + await create_suggestions(suggestions) + except Exception: + pass + finally: + _suggestions_generating.discard(topic) + + +async def generate_baustein_detail(baustein_id: str, topic: str, title: str) -> None: + try: + prompt = _build_baustein_detail_prompt(topic, title) + 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 5bc9d76..7f4be73 100644 --- a/backend/models.py +++ b/backend/models.py @@ -32,3 +32,30 @@ class GuideResponse(BaseModel): pdf_path: str | None = None created_at: str updated_at: str + + +class BausteinCreateRequest(BaseModel): + topic: str = Field(min_length=1, max_length=100) + title: str = Field(min_length=1, max_length=200) + + +class BausteinResponse(BaseModel): + id: str + topic: str + title: str + description: str + purpose: str + example: str + created_at: str + updated_at: str + + +class SuggestionResponse(BaseModel): + id: str + topic: str + title: str + description: str + purpose: str + example: str + status: str + created_at: str diff --git a/backend/routes.py b/backend/routes.py index 9f75c34..e3ffcd8 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -7,9 +7,16 @@ from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse from config import FORMAT_META, STORAGE_DIR -from database import create_guide, delete_guide, get_guide, list_guides -from generator import generate_guide, rework_guide, cancel_guide -from models import GuideCreateRequest, GuideReworkRequest, GuideResponse +from database import ( + create_guide, delete_guide, get_guide, list_guides, + 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 models import ( + GuideCreateRequest, GuideReworkRequest, GuideResponse, + BausteinCreateRequest, BausteinResponse, SuggestionResponse, +) router = APIRouter(prefix="/api") @@ -108,3 +115,92 @@ async def remove(guide_id: str): Path(guide["pdf_path"]).unlink(missing_ok=True) await delete_guide(guide_id) return {"ok": True} + + +# --- Bausteine --- + +@router.get("/bausteine", response_model=list[BausteinResponse]) +async def get_bausteine(topic: str): + return await list_bausteine(topic) + + +@router.post("/bausteine", response_model=BausteinResponse) +async def add_baustein(req: BausteinCreateRequest): + now = datetime.now(timezone.utc).isoformat() + baustein = { + "id": str(uuid.uuid4()), + "topic": req.topic.strip(), + "title": req.title.strip(), + "description": "", + "purpose": "", + "example": "", + "created_at": now, + "updated_at": now, + } + await db_create_baustein(baustein) + asyncio.create_task(generate_baustein_detail(baustein["id"], baustein["topic"], baustein["title"])) + return baustein + + +@router.delete("/bausteine/{baustein_id}") +async def remove_baustein(baustein_id: str): + b = await get_baustein(baustein_id) + if b is None: + raise HTTPException(404, "Baustein nicht gefunden") + await db_delete_baustein(baustein_id) + return {"ok": True} + + +# --- Baustein Suggestions --- + +@router.get("/bausteine/suggestions", response_model=list[SuggestionResponse]) +async def get_suggestions(topic: str): + return await list_suggestions(topic) + + +@router.post("/bausteine/suggestions/generate") +async def trigger_suggestions(topic: str): + if is_suggestions_generating(topic): + return {"ok": True, "status": "already_generating"} + guides = await list_guides() + html_paths = [] + for g in guides: + if g["topic"] == topic and g["status"] == "done" and g["html_path"]: + html_paths.append(Path(g["html_path"])) + asyncio.create_task(generate_suggestions(topic, html_paths)) + return {"ok": True} + + +@router.get("/bausteine/suggestions/status") +async def suggestions_status(topic: str): + return {"generating": is_suggestions_generating(topic)} + + +@router.post("/bausteine/suggestions/{suggestion_id}/add") +async def accept_suggestion(suggestion_id: str): + s = await get_suggestion(suggestion_id) + if s is None: + raise HTTPException(404, "Vorschlag nicht gefunden") + now = datetime.now(timezone.utc).isoformat() + baustein = { + "id": str(uuid.uuid4()), + "topic": s["topic"], + "title": s["title"], + "description": s["description"], + "purpose": s["purpose"], + "example": s["example"], + "created_at": now, + "updated_at": now, + } + await db_create_baustein(baustein) + await delete_suggestion(suggestion_id) + return baustein + + +@router.post("/bausteine/suggestions/{suggestion_id}/ignore") +async def ignore_suggestion(suggestion_id: str): + s = await get_suggestion(suggestion_id) + if s is None: + raise HTTPException(404, "Vorschlag nicht gefunden") + await update_suggestion(suggestion_id, status="ignored") + return {"ok": True} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index fdf2b0b..8614099 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,11 +3,13 @@ import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { fetchGuides, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, reworkGuide as apiRework } from './api.js' import TopicSidebar from './components/TopicSidebar.vue' import TopicDetail from './components/TopicDetail.vue' +import BausteineView from './components/BausteineView.vue' const guides = ref([]) const manualTopics = ref([]) const selectedTopic = ref(null) const previewGuide = ref(null) +const showBausteine = ref(false) let pollTimer = null const topics = computed(() => { @@ -62,6 +64,7 @@ function autoPreview() { function selectTopic(topic) { selectedTopic.value = topic previewGuide.value = null + showBausteine.value = false nextTick(autoPreview) } @@ -88,6 +91,12 @@ async function handleRework({ guideId, instructions }) { function handlePreview(guide) { previewGuide.value = guide + showBausteine.value = false +} + +function handleShowBausteine() { + showBausteine.value = true + previewGuide.value = null } async function handleDeleteGuide(guideId) { @@ -161,6 +170,7 @@ onUnmounted(() => { :selectedTopic="selectedTopic" :guidesByFormat="guidesByFormat" :allGuides="guides" + :bausteineActive="showBausteine" @select="selectTopic" @create="createTopic" @formatClick="handleFormatClick" @@ -169,9 +179,14 @@ onUnmounted(() => { @deleteGuide="handleDeleteGuide" @preview="handlePreview" @rework="handleRework" + @showBausteine="handleShowBausteine" + /> +
diff --git a/frontend/src/api.js b/frontend/src/api.js index e2b98db..a05674c 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -43,3 +43,44 @@ export function pdfUrl(id) { export function htmlUrl(id) { return `${BASE}/guides/${id}/html` } + +export async function fetchBausteine(topic) { + const res = await fetch(`${BASE}/bausteine?topic=${encodeURIComponent(topic)}`) + return res.json() +} + +export async function createBaustein(topic, title) { + const res = await fetch(`${BASE}/bausteine`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ topic, title }), + }) + return res.json() +} + +export async function deleteBaustein(id) { + await fetch(`${BASE}/bausteine/${id}`, { method: 'DELETE' }) +} + +export async function fetchSuggestions(topic) { + const res = await fetch(`${BASE}/bausteine/suggestions?topic=${encodeURIComponent(topic)}`) + return res.json() +} + +export async function generateSuggestions(topic) { + await fetch(`${BASE}/bausteine/suggestions/generate?topic=${encodeURIComponent(topic)}`, { method: 'POST' }) +} + +export async function fetchSuggestionsStatus(topic) { + const res = await fetch(`${BASE}/bausteine/suggestions/status?topic=${encodeURIComponent(topic)}`) + return res.json() +} + +export async function addSuggestion(id) { + const res = await fetch(`${BASE}/bausteine/suggestions/${id}/add`, { method: 'POST' }) + return res.json() +} + +export async function ignoreSuggestion(id) { + await fetch(`${BASE}/bausteine/suggestions/${id}/ignore`, { method: 'POST' }) +} diff --git a/frontend/src/components/BausteineView.vue b/frontend/src/components/BausteineView.vue new file mode 100644 index 0000000..d7cb76e --- /dev/null +++ b/frontend/src/components/BausteineView.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/frontend/src/components/TopicSidebar.vue b/frontend/src/components/TopicSidebar.vue index ae8e2fd..d172692 100644 --- a/frontend/src/components/TopicSidebar.vue +++ b/frontend/src/components/TopicSidebar.vue @@ -6,9 +6,10 @@ const props = defineProps({ selectedTopic: { type: String, default: null }, guidesByFormat: { type: Object, default: () => ({}) }, allGuides: { type: Array, default: () => [] }, + bausteineActive: { type: Boolean, default: false }, }) -const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework']) +const emit = defineEmits(['select', 'create', 'formatClick', 'deleteTopic', 'cancelGuide', 'deleteGuide', 'preview', 'rework', 'showBausteine']) const formats = [ { key: 'OnePager', label: 'OnePager' }, @@ -155,6 +156,13 @@ function submit() { />
+
+ +
@@ -399,4 +407,34 @@ function submit() { 0%, 100% { opacity: 1; } 50% { opacity: 0.65; } } + +.bausteine-btn-wrapper { + padding: 0.5rem 0.75rem; + border-top: 1px solid #e2e5e9; +} + +.bausteine-btn { + width: 100%; + padding: 8px 12px; + border: 1px solid #d8dde3; + border-radius: 6px; + background: #f8f9fb; + color: #4b5563; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all 0.15s; +} + +.bausteine-btn:hover { + background: #ede9fe; + border-color: #a5b4fc; + color: #4f46e5; +} + +.bausteine-btn.active { + background: #6366f1; + border-color: #6366f1; + color: white; +} diff --git a/templates/Format/Baustein.md b/templates/Format/Baustein.md new file mode 100644 index 0000000..c6accff --- /dev/null +++ b/templates/Format/Baustein.md @@ -0,0 +1,104 @@ +``` +BAUSTEIN-CARD-STIL (HTML+CSS, in Browser anzeigbar) + +ZWECK +- Einzelner Baustein als kompakte Card-Darstellung +- Schnelles Nachschlagen einzelner Konzepte +- Card ist Teil einer größeren Baustein-Sammlung +- Nicht zum Lernen, sondern zum Wiedererkennen +- Pro Card genau 1 Baustein + +FORMAT +- HTML mit eingebettetem CSS +- Im Browser anzeigbar (kein PDF-Output) +- Card auf hellgrauem Hintergrund zentriert +- Max-width 1000px, padding 28px 36px +- Border-radius 12px, dezenter Schatten +- Page-Hintergrund #f0f0f5 + +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) + +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. + +TYPOGRAFIE +- Body: -apple-system, "Segoe UI", sans-serif, 14px, line-height 1.5 +- Titel h1: 28px, font-weight 800, letter-spacing -0.5px, line-height 1.1 +- Beschreibung: 14px, line-height 1.55 +- Zweck: 14px, italic, color #5a6470 +- Code: "SF Mono", Consolas, monospace, 12px, line-height 1.5 +- Code-Label: 10px, uppercase, letter-spacing 1px + +FARBEN +- Card-Hintergrund: #ffffff +- Page-Hintergrund: #f0f0f5 +- Text: #1a1a1a +- Muted (Zweck): #5a6470 +- Code-Hintergrund: #1e2a3a +- Code-Text: #e6e6e6 +- Syntax-Highlighting: + - Keywords (.k): #ff79c6 (pink) + - Variablen (.v): #ffb86c (orange) + - Strings (.s): #f1c40f (gelb) + - Funktionen (.f): #50fa7b (grün) + - Typen (.t): #8be9fd (cyan) + - Kommentare (.c): #6b8aae italic +- Label-Farbe in Code: #8be9fd (cyan) + +INHALTLICHE PRINZIPIEN +- Titel: nur der Baustein-Name, kein Präfix, keine Variante +- 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) + +LAYOUT-DETAILS +- Header und Body keine separaten Sektionen, alles in einer Card +- Titel zuerst, darunter direkt Beschreibung, darunter Zweck (mit margin-bottom) +- 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 +- Sehr kurz: ideal 3-6 Zeilen, max 8 Zeilen +- Mit kurzem Label was die Variante zeigt +- 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 +- Mehrere Sätze für Beschreibung oder Zweck +- Performance-Tipps, Trade-Offs, Edge Cases +- Verwandte Bausteine, Varianten, Anti-Patterns +- Tabellen, Listen, Aufzählungen +- Icons, Emojis, Symbole +- Komplexe Code-Beispiele (über 8 Zeilen) + +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 + +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)? + - Beschreibung und Zweck unter 15 Wörtern? + - Code-Beispiele unter 8 Zeilen? + - Labels über Code-Blöcken kurz und prägnant? + - Card kompakt, kein leerer Raum? +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 new file mode 100644 index 0000000..b4a7107 --- /dev/null +++ b/templates/Referenz/Baustein.md @@ -0,0 +1,119 @@ +``` + + + + +Baustein: for-Schleife + + + + +
+ +

for-Schleife

+ +

Führt einen Code-Block wiederholt aus.

+ +

Wiederholung wenn Anzahl Durchläufe bekannt ist oder Index gebraucht wird.

+ +
+ +
Vorwärts zählenfor ($i = 0; $i < 10; $i++) { + 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]; +}
+ +
+ +
+ + + +``` \ No newline at end of file