From 067d7229de71c6d219395b8316c5930545a16092 Mon Sep 17 00:00:00 2001 From: Team3 Date: Fri, 29 May 2026 17:58:43 +0200 Subject: [PATCH] update --- backend/config.py | 2 +- backend/database.py | 17 ++++++- backend/generator.py | 54 ++++++++++++++++++++- backend/models.py | 5 ++ backend/routes.py | 20 +++++++- frontend/src/api.js | 13 +++++ frontend/src/components/BausteineView.vue | 59 +++++++++++++++++++---- templates/Format/Baustein.md | 27 +++++++++++ 8 files changed, 183 insertions(+), 14 deletions(-) diff --git a/backend/config.py b/backend/config.py index e683245..52125c9 100644 --- a/backend/config.py +++ b/backend/config.py @@ -31,4 +31,4 @@ CLAUDE_CLI = "claude" MODEL_GUIDE = "claude-opus-4-8" MODEL_BAUSTEIN_GEN = "claude-sonnet-4-6" -MODEL_BAUSTEIN_REWORK = "claude-haiku-4-5" +MODEL_BAUSTEIN_REWORK = "claude-sonnet-4-6" diff --git a/backend/database.py b/backend/database.py index 3bd6f84..8810ed4 100644 --- a/backend/database.py +++ b/backend/database.py @@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS bausteine ( description TEXT NOT NULL DEFAULT '', purpose TEXT NOT NULL DEFAULT '', example TEXT NOT NULL DEFAULT '', + sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) @@ -62,6 +63,10 @@ async def init_db(): columns = {row[1] for row in await cursor.fetchall()} if "instructions" not in columns: await db.execute("ALTER TABLE guides ADD COLUMN instructions TEXT NOT NULL DEFAULT ''") + cursor = await db.execute("PRAGMA table_info(bausteine)") + columns = {row[1] for row in await cursor.fetchall()} + if "sort_order" not in columns: + await db.execute("ALTER TABLE bausteine ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0") await db.execute( "UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' " "WHERE status IN ('queued', 'generating')" @@ -153,12 +158,22 @@ async def create_baustein(baustein: dict) -> dict: 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,) + "SELECT * FROM bausteine WHERE topic = ? ORDER BY sort_order ASC, created_at ASC", (topic,) ) rows = await cursor.fetchall() return [_row_to_dict(row, cursor) for row in rows] +async def update_baustein_sort_orders(topic: str, order_map: dict) -> None: + db = await get_db() + for baustein_id, order in order_map.items(): + await db.execute( + "UPDATE bausteine SET sort_order = ? WHERE id = ? AND topic = ?", + (order, baustein_id, topic), + ) + await db.commit() + + async def get_baustein(baustein_id: str) -> dict | None: db = await get_db() cursor = await db.execute("SELECT * FROM bausteine WHERE id = ?", (baustein_id,)) diff --git a/backend/generator.py b/backend/generator.py index 4639601..905376c 100644 --- a/backend/generator.py +++ b/backend/generator.py @@ -25,6 +25,7 @@ from database import ( delete_pending_suggestions, list_bausteine, update_baustein, + update_baustein_sort_orders, ) from paths import final_paths, temp_paths @@ -327,12 +328,17 @@ async def _fail(guide_id: str, msg: str) -> None: # --- Bausteine --- _suggestions_generating: set[str] = set() +_sorting: set[str] = set() def is_suggestions_generating(topic: str) -> bool: return topic in _suggestions_generating +def is_sorting(topic: str) -> bool: + return topic in _sorting + + def _parse_json(text: str): text = text.strip() text = re.sub(r"^```(?:json)?\s*", "", text) @@ -484,6 +490,8 @@ async def rework_baustein(baustein_id: str, topic: str, title: str, current: dic def _build_baustein_rework_prompt(topic: str, title: str, current: dict, instructions: str) -> str: + spec = (TEMPLATES_DIR / "Format" / "Baustein.md").read_text(encoding="utf-8") + current_json = json.dumps({ "title": title, "description": current.get("description", ""), @@ -499,9 +507,53 @@ AKTUELLER STAND: ANWEISUNGEN VOM NUTZER: {instructions} +FORMAT-SPEZIFIKATION: +{spec} + Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "title", "description", "purpose", "examples". "examples" ist ein Array mit Objekten {{"label": "...", "code": "..."}}. -Kein weiterer Text, nur das JSON. +Orientiere dich an der Spezifikation. Kein weiterer Text, nur das JSON. """ + + +def _build_sort_prompt(topic: str, bausteine: list[dict], instructions: str) -> str: + items = "\n".join( + f"- id={b['id']} | {b['title']} | {b['description']} | {b['purpose']}" + for b in bausteine + ) + if instructions: + criterion = f"Sortiere die folgenden Bausteine zum Thema \"{topic}\" STRIKT nach diesem Kriterium:\n\n{instructions}" + else: + criterion = f"Sortiere die folgenden Bausteine zum Thema \"{topic}\" von Anfaenger zu Experte (erstes = einfachster, letztes = komplexester)." + + return f"""{criterion} + +BAUSTEINE: +{items} + +Antworte AUSSCHLIESSLICH mit einem JSON-Array der IDs in der gewuenschten Reihenfolge. +Beispiel: [\"id1\", \"id2\", \"id3\"] + +Kein weiterer Text, nur das JSON-Array. +""" + + +async def sort_bausteine(topic: str, bausteine: list[dict], instructions: str = "") -> None: + _sorting.add(topic) + try: + prompt = _build_sort_prompt(topic, bausteine, instructions) + returncode, stdout, stderr = await _run_claude("sort-" + topic, prompt, 300, tools=None, model=MODEL_BAUSTEIN_GEN) + if returncode != 0: + return + ids = _parse_json(stdout) + if not isinstance(ids, list): + return + order_map = {bid: i for i, bid in enumerate(ids) if isinstance(bid, str)} + if order_map: + await update_baustein_sort_orders(topic, order_map) + except Exception as e: + print(f"[sort] topic={topic} Exception: {type(e).__name__}: {e}") + finally: + _sorting.discard(topic) diff --git a/backend/models.py b/backend/models.py index d478412..3b2a598 100644 --- a/backend/models.py +++ b/backend/models.py @@ -49,10 +49,15 @@ class BausteinResponse(BaseModel): description: str purpose: str example: str + sort_order: int = 0 created_at: str updated_at: str +class BausteinSortRequest(BaseModel): + instructions: str = Field(default="", max_length=2000) + + class SuggestionResponse(BaseModel): id: str topic: str diff --git a/backend/routes.py b/backend/routes.py index 8322d7a..2c120b6 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, rework_baustein, is_suggestions_generating +from generator import generate_guide, rework_guide, cancel_guide, generate_suggestions, generate_baustein_detail, rework_baustein, sort_bausteine, is_suggestions_generating, is_sorting from models import ( GuideCreateRequest, GuideReworkRequest, GuideResponse, - BausteinCreateRequest, BausteinReworkRequest, BausteinResponse, SuggestionResponse, + BausteinCreateRequest, BausteinReworkRequest, BausteinSortRequest, BausteinResponse, SuggestionResponse, ) from paths import final_paths @@ -167,6 +167,22 @@ async def rework_baustein_route(baustein_id: str, req: BausteinReworkRequest): return {"ok": True} +@router.post("/bausteine/sort") +async def sort_bausteine_route(topic: str, req: BausteinSortRequest): + if is_sorting(topic): + return {"ok": True, "status": "already_sorting"} + bausteine = await list_bausteine(topic) + if not bausteine: + return {"ok": True} + asyncio.create_task(sort_bausteine(topic, bausteine, req.instructions.strip())) + return {"ok": True} + + +@router.get("/bausteine/sort/status") +async def sort_status(topic: str): + return {"sorting": is_sorting(topic)} + + # --- Baustein Suggestions --- @router.get("/bausteine/suggestions", response_model=list[SuggestionResponse]) diff --git a/frontend/src/api.js b/frontend/src/api.js index 07edf4b..1f69021 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -70,6 +70,19 @@ export async function reworkBaustein(id, instructions) { }) } +export async function sortBausteine(topic, instructions = '') { + await fetch(`${BASE}/bausteine/sort?topic=${encodeURIComponent(topic)}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ instructions }), + }) +} + +export async function fetchSortStatus(topic) { + const res = await fetch(`${BASE}/bausteine/sort/status?topic=${encodeURIComponent(topic)}`) + return res.json() +} + 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 00d3dd2..a396015 100644 --- a/frontend/src/components/BausteineView.vue +++ b/frontend/src/components/BausteineView.vue @@ -5,6 +5,8 @@ import { createBaustein, deleteBaustein, reworkBaustein, + sortBausteine, + fetchSortStatus, fetchSuggestions, generateSuggestions, fetchSuggestionsStatus, @@ -21,6 +23,9 @@ const suggestions = ref([]) const suggestionsLoading = ref(false) const newTitle = ref('') const newInfo = ref('') +const sortInfo = ref('') +const sortingActive = ref(false) +let sortPollTimer = null const reworkInputs = ref({}) const reworkingIds = ref(new Set()) const reworkingSnapshots = new Map() @@ -95,6 +100,26 @@ async function handleRegenerate() { startPolling() } +async function handleSort() { + sortingActive.value = true + const info = sortInfo.value.trim() + await sortBausteine(props.topic, info) + startSortPolling() +} + +function startSortPolling() { + if (sortPollTimer) return + sortPollTimer = setInterval(async () => { + const status = await fetchSortStatus(props.topic) + if (!status.sorting) { + sortingActive.value = false + clearInterval(sortPollTimer) + sortPollTimer = null + bausteine.value = await fetchBausteine(props.topic) + } + }, 3000) +} + async function handleRework(b) { const instructions = (reworkInputs.value[b.id] || '').trim() if (!instructions) return @@ -147,6 +172,10 @@ function stopPolling() { clearInterval(bausteinPollTimer) bausteinPollTimer = null } + if (sortPollTimer) { + clearInterval(sortPollTimer) + sortPollTimer = null + } } async function init() { @@ -177,12 +206,22 @@ onUnmounted(stopPolling) placeholder="Thema…" @keyup.enter="handleAdd" /> - + @keyup.enter="handleAdd" + /> + +

Ordnen

+ +
@@ -297,8 +336,13 @@ onUnmounted(stopPolling) margin: 0; } -.new-card input, -.new-card textarea { +.new-card-section { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid #e2e5e9; +} + +.new-card input { width: 100%; padding: 8px 10px; border: 1px solid #d8dde3; @@ -306,11 +350,9 @@ onUnmounted(stopPolling) font-size: 0.85rem; font-family: inherit; outline: none; - resize: vertical; } -.new-card input:focus, -.new-card textarea:focus { +.new-card input:focus { border-color: #6366f1; } @@ -323,7 +365,6 @@ onUnmounted(stopPolling) font-size: 0.85rem; font-weight: 600; cursor: pointer; - margin-top: auto; } .new-btn:disabled { diff --git a/templates/Format/Baustein.md b/templates/Format/Baustein.md index ad66500..6b4b949 100644 --- a/templates/Format/Baustein.md +++ b/templates/Format/Baustein.md @@ -83,6 +83,32 @@ CODE-BEISPIEL - Mit kurzem Label oben (2-4 Wörter) - Syntax-Highlighting durch span-Klassen (.k, .v, .s, etc.) +HTML-ENTITIES IM CODE (PFLICHT bei HTML/XML/JSX/Vue/JSX-ähnlichem Code) +- Wenn das Code-Beispiel SELBST HTML, XML, JSX oder ähnliche Tag-Syntax zeigt, MÜSSEN spitze Klammern als HTML-Entities geschrieben werden: + - `<` → `<` + - `>` → `>` + - `&` → `&` +- Grund: der Code wird via v-html im Browser gerendert. Rohe `

` werden sonst als echtes DOM-Element interpretiert und verschwinden. +- Gut: `<h1>Text</h1>` +- Schlecht: `

Text

` +- Schlecht: `

Text

` (komplett ohne Spans und ohne Entities) +- Diese Regel gilt NUR für die Inhalte des Code-Beispiels, NICHT für die ``-Wrapper selbst + +KONKRETES BEISPIEL — Baustein "Header" (HTML) +```json +{ + "title": "Header", + "description": "Definiert eine Überschrift.", + "purpose": "Strukturiert die Seiteninhalte.", + "examples": [ + { + "label": "Alle Header", + "code": "<h1>Hauptüberschrift</h1>\n<h2>Kapitel</h2>\n<h3>Unterabschnitt</h3>" + } + ] +} +``` + VERMEIDEN - Lange Erklärungstexte - Mehrere Sätze für Beschreibung oder Zweck @@ -111,6 +137,7 @@ GENERIERUNG MIT FEEDBACK-LOOP - Label über Code-Block kurz und prägnant? - Card kompakt, kein leerer Raum? - Ist das gewählte Beispiel wirklich das typischste? + - Bei HTML/XML/JSX-Code: alle `<` und `>` als `<` und `>` geschrieben? 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