update
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
<BausteineView
|
||||
v-if="selectedTopic && showBausteine"
|
||||
:topic="selectedTopic"
|
||||
/>
|
||||
<TopicDetail
|
||||
v-if="selectedTopic"
|
||||
v-else-if="selectedTopic"
|
||||
:previewGuide="previewGuide"
|
||||
/>
|
||||
<div v-else class="empty-main">
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
460
frontend/src/components/BausteineView.vue
Normal file
460
frontend/src/components/BausteineView.vue
Normal file
@@ -0,0 +1,460 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import {
|
||||
fetchBausteine,
|
||||
createBaustein,
|
||||
deleteBaustein,
|
||||
fetchSuggestions,
|
||||
generateSuggestions,
|
||||
fetchSuggestionsStatus,
|
||||
addSuggestion,
|
||||
ignoreSuggestion,
|
||||
} from '../api.js'
|
||||
|
||||
const props = defineProps({
|
||||
topic: { type: String, required: true },
|
||||
})
|
||||
|
||||
const bausteine = ref([])
|
||||
const suggestions = ref([])
|
||||
const suggestionsLoading = ref(false)
|
||||
const newTitle = ref('')
|
||||
let pollTimer = null
|
||||
|
||||
const pendingSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'pending'))
|
||||
const ignoredSuggestions = computed(() => suggestions.value.filter((s) => s.status === 'ignored'))
|
||||
|
||||
function parseExamples(example) {
|
||||
if (!example) return []
|
||||
try {
|
||||
const parsed = JSON.parse(example)
|
||||
if (Array.isArray(parsed)) return parsed
|
||||
} catch {}
|
||||
return [{ label: '', code: example }]
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
const [b, s] = await Promise.all([fetchBausteine(props.topic), fetchSuggestions(props.topic)])
|
||||
bausteine.value = b
|
||||
suggestions.value = s
|
||||
}
|
||||
|
||||
async function checkAndGenerate() {
|
||||
const status = await fetchSuggestionsStatus(props.topic)
|
||||
if (status.generating) {
|
||||
suggestionsLoading.value = true
|
||||
startPolling()
|
||||
} else if (suggestions.value.length === 0) {
|
||||
suggestionsLoading.value = true
|
||||
await generateSuggestions(props.topic)
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
const title = newTitle.value.trim()
|
||||
if (!title) return
|
||||
newTitle.value = ''
|
||||
const created = await createBaustein(props.topic, title)
|
||||
bausteine.value.push(created)
|
||||
}
|
||||
|
||||
async function handleDelete(id) {
|
||||
await deleteBaustein(id)
|
||||
bausteine.value = bausteine.value.filter((b) => b.id !== id)
|
||||
}
|
||||
|
||||
async function handleAccept(s) {
|
||||
const created = await addSuggestion(s.id)
|
||||
suggestions.value = suggestions.value.filter((x) => x.id !== s.id)
|
||||
bausteine.value.push(created)
|
||||
}
|
||||
|
||||
async function handleIgnore(s) {
|
||||
await ignoreSuggestion(s.id)
|
||||
const idx = suggestions.value.findIndex((x) => x.id === s.id)
|
||||
if (idx !== -1) suggestions.value[idx].status = 'ignored'
|
||||
}
|
||||
|
||||
async function handleRestore(s) {
|
||||
const created = await addSuggestion(s.id)
|
||||
suggestions.value = suggestions.value.filter((x) => x.id !== s.id)
|
||||
bausteine.value.push(created)
|
||||
}
|
||||
|
||||
async function handleRegenerate() {
|
||||
suggestionsLoading.value = true
|
||||
await generateSuggestions(props.topic)
|
||||
startPolling()
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
stopPolling()
|
||||
pollTimer = setInterval(async () => {
|
||||
const status = await fetchSuggestionsStatus(props.topic)
|
||||
if (!status.generating) {
|
||||
suggestionsLoading.value = false
|
||||
stopPolling()
|
||||
await loadData()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function stopPolling() {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await loadData()
|
||||
await checkAndGenerate()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.topic,
|
||||
() => {
|
||||
stopPolling()
|
||||
suggestionsLoading.value = false
|
||||
init()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(init)
|
||||
onUnmounted(stopPolling)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bausteine-view">
|
||||
<div class="bausteine-grid">
|
||||
<div class="card new-card">
|
||||
<input
|
||||
v-model="newTitle"
|
||||
placeholder="Neuer Baustein…"
|
||||
@keyup.enter="handleAdd"
|
||||
/>
|
||||
<button @click="handleAdd" :disabled="!newTitle.trim()">+</button>
|
||||
</div>
|
||||
|
||||
<div v-for="b in bausteine" :key="b.id" class="card">
|
||||
<div class="card-header">
|
||||
<h3>{{ b.title }}</h3>
|
||||
<button class="card-delete" @click="handleDelete(b.id)">×</button>
|
||||
</div>
|
||||
<p v-if="b.description" class="desc">{{ b.description }}</p>
|
||||
<p v-if="b.purpose" class="purpose">{{ b.purpose }}</p>
|
||||
<div v-if="b.example" class="examples">
|
||||
<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>
|
||||
<pre>{{ ex.code }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!b.description && !b.purpose" class="loading-text">Wird generiert…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="pendingSuggestions.length || suggestionsLoading" class="divider" />
|
||||
|
||||
<div v-if="suggestionsLoading" class="loading-indicator">Generiere Vorschläge…</div>
|
||||
|
||||
<div v-if="pendingSuggestions.length" class="suggestions-section">
|
||||
<div class="section-header">
|
||||
<h3>Vorschläge</h3>
|
||||
<button class="regenerate-btn" @click="handleRegenerate" :disabled="suggestionsLoading">Neu generieren</button>
|
||||
</div>
|
||||
<div class="suggestions-grid">
|
||||
<div v-for="s in pendingSuggestions" :key="s.id" class="card suggestion-card">
|
||||
<h3>{{ s.title }}</h3>
|
||||
<p v-if="s.description" class="desc">{{ s.description }}</p>
|
||||
<p v-if="s.purpose" class="purpose">{{ s.purpose }}</p>
|
||||
<div v-if="s.example" class="examples">
|
||||
<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>
|
||||
<pre>{{ ex.code }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="suggestion-actions">
|
||||
<button class="btn-add" @click="handleAccept(s)">Hinzufügen</button>
|
||||
<button class="btn-ignore" @click="handleIgnore(s)">Ignorieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr v-if="ignoredSuggestions.length" class="divider" />
|
||||
|
||||
<div v-if="ignoredSuggestions.length" class="ignored-section">
|
||||
<h3>Ignoriert</h3>
|
||||
<div v-for="s in ignoredSuggestions" :key="s.id" class="ignored-item">
|
||||
<span>{{ s.title }}</span>
|
||||
<button class="btn-restore" @click="handleRestore(s)">Hinzufügen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.bausteine-view {
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.bausteine-grid,
|
||||
.suggestions-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e5e9;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.new-card {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #f8f9fb;
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
.new-card input {
|
||||
flex: 1;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d8dde3;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.new-card input:focus {
|
||||
border-color: #6366f1;
|
||||
}
|
||||
|
||||
.new-card button {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.new-card button:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-header h3 {
|
||||
font-size: 0.95rem;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.card-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #991b1b;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.card:hover .card-delete {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 0.85rem;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.purpose {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.examples {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e2a3a;
|
||||
color: #e6e6e6;
|
||||
font-family: "SF Mono", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.code-label {
|
||||
font-family: -apple-system, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #8be9fd;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 0.8rem;
|
||||
color: #9ca3af;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: none;
|
||||
border-top: 1px solid #e2e5e9;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
font-size: 0.85rem;
|
||||
color: #92400e;
|
||||
background: #fef3c7;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 1rem;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-header h3 {
|
||||
font-size: 0.9rem;
|
||||
color: #4b5563;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.regenerate-btn {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #d8dde3;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.regenerate-btn:hover {
|
||||
border-color: #6366f1;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.regenerate-btn:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.suggestion-card {
|
||||
border-color: #c7d2fe;
|
||||
background: #fafafe;
|
||||
}
|
||||
|
||||
.suggestion-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: auto;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-add {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #34d399;
|
||||
border-radius: 4px;
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-add:hover {
|
||||
background: #a7f3d0;
|
||||
}
|
||||
|
||||
.btn-ignore {
|
||||
padding: 4px 10px;
|
||||
border: 1px solid #d8dde3;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #6b7280;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-ignore:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.ignored-section h3 {
|
||||
font-size: 0.85rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ignored-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.4rem 0;
|
||||
font-size: 0.85rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.btn-restore {
|
||||
padding: 2px 8px;
|
||||
border: 1px solid #d8dde3;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
color: #6b7280;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-restore:hover {
|
||||
border-color: #34d399;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.65; }
|
||||
}
|
||||
</style>
|
||||
@@ -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() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bausteine-btn-wrapper">
|
||||
<button
|
||||
class="bausteine-btn"
|
||||
:class="{ active: bausteineActive }"
|
||||
@click="emit('showBausteine')"
|
||||
>Bausteine</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
|
||||
104
templates/Format/Baustein.md
Normal file
104
templates/Format/Baustein.md
Normal file
@@ -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?
|
||||
```
|
||||
119
templates/Referenz/Baustein.md
Normal file
119
templates/Referenz/Baustein.md
Normal file
@@ -0,0 +1,119 @@
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Baustein: for-Schleife</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", sans-serif;
|
||||
background: #f0f0f5;
|
||||
padding: 40px 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
border-radius: 12px;
|
||||
padding: 28px 36px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||||
color: #1a1a1a;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.5px;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.beschreibung {
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.zweck {
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
color: #5a6470;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.examples {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1e2a3a;
|
||||
color: #e6e6e6;
|
||||
font-family: "SF Mono", Consolas, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
padding: 14px 16px;
|
||||
border-radius: 8px;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.code-block .label {
|
||||
font-family: -apple-system, sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
color: #8be9fd;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.k { color: #ff79c6; }
|
||||
.v { color: #ffb86c; }
|
||||
.n { color: #ffb86c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="card">
|
||||
|
||||
<h1>for-Schleife</h1>
|
||||
|
||||
<p class="beschreibung">Führt einen Code-Block wiederholt aus.</p>
|
||||
|
||||
<p class="zweck">Wiederholung wenn Anzahl Durchläufe bekannt ist oder Index gebraucht wird.</p>
|
||||
|
||||
<div class="examples">
|
||||
|
||||
<div class="code-block"><span class="label">Vorwärts zählen</span><span class="k">for</span> (<span class="v">$i</span> = <span class="n">0</span>; <span class="v">$i</span> < <span class="n">10</span>; <span class="v">$i</span>++) {
|
||||
<span class="k">echo</span> <span class="v">$i</span>;
|
||||
}</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> > <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> <= <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> < <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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
Reference in New Issue
Block a user