This commit is contained in:
Team3
2026-05-29 17:58:43 +02:00
parent a826e9f6b3
commit 067d7229de
8 changed files with 183 additions and 14 deletions

View File

@@ -31,4 +31,4 @@ CLAUDE_CLI = "claude"
MODEL_GUIDE = "claude-opus-4-8" MODEL_GUIDE = "claude-opus-4-8"
MODEL_BAUSTEIN_GEN = "claude-sonnet-4-6" MODEL_BAUSTEIN_GEN = "claude-sonnet-4-6"
MODEL_BAUSTEIN_REWORK = "claude-haiku-4-5" MODEL_BAUSTEIN_REWORK = "claude-sonnet-4-6"

View File

@@ -24,6 +24,7 @@ CREATE TABLE IF NOT EXISTS bausteine (
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
purpose TEXT NOT NULL DEFAULT '', purpose TEXT NOT NULL DEFAULT '',
example TEXT NOT NULL DEFAULT '', example TEXT NOT NULL DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_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()} columns = {row[1] for row in await cursor.fetchall()}
if "instructions" not in columns: if "instructions" not in columns:
await db.execute("ALTER TABLE guides ADD COLUMN instructions TEXT NOT NULL DEFAULT ''") 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( await db.execute(
"UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' " "UPDATE guides SET status = 'error', progress = NULL, error_msg = 'Server-Neustart' "
"WHERE status IN ('queued', 'generating')" "WHERE status IN ('queued', 'generating')"
@@ -153,12 +158,22 @@ async def create_baustein(baustein: dict) -> dict:
async def list_bausteine(topic: str) -> list[dict]: async def list_bausteine(topic: str) -> list[dict]:
db = await get_db() db = await get_db()
cursor = await db.execute( 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() rows = await cursor.fetchall()
return [_row_to_dict(row, cursor) for row in rows] 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: async def get_baustein(baustein_id: str) -> dict | None:
db = await get_db() db = await get_db()
cursor = await db.execute("SELECT * FROM bausteine WHERE id = ?", (baustein_id,)) cursor = await db.execute("SELECT * FROM bausteine WHERE id = ?", (baustein_id,))

View File

@@ -25,6 +25,7 @@ from database import (
delete_pending_suggestions, delete_pending_suggestions,
list_bausteine, list_bausteine,
update_baustein, update_baustein,
update_baustein_sort_orders,
) )
from paths import final_paths, temp_paths from paths import final_paths, temp_paths
@@ -327,12 +328,17 @@ async def _fail(guide_id: str, msg: str) -> None:
# --- Bausteine --- # --- Bausteine ---
_suggestions_generating: set[str] = set() _suggestions_generating: set[str] = set()
_sorting: set[str] = set()
def is_suggestions_generating(topic: str) -> bool: def is_suggestions_generating(topic: str) -> bool:
return topic in _suggestions_generating return topic in _suggestions_generating
def is_sorting(topic: str) -> bool:
return topic in _sorting
def _parse_json(text: str): def _parse_json(text: str):
text = text.strip() text = text.strip()
text = re.sub(r"^```(?:json)?\s*", "", text) 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: 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({ current_json = json.dumps({
"title": title, "title": title,
"description": current.get("description", ""), "description": current.get("description", ""),
@@ -499,9 +507,53 @@ AKTUELLER STAND:
ANWEISUNGEN VOM NUTZER: ANWEISUNGEN VOM NUTZER:
{instructions} {instructions}
FORMAT-SPEZIFIKATION:
{spec}
Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "title", "description", "purpose", "examples". Antworte AUSSCHLIESSLICH mit einem JSON-Objekt mit den Feldern "title", "description", "purpose", "examples".
"examples" ist ein Array mit Objekten {{"label": "...", "code": "..."}}. "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)

View File

@@ -49,10 +49,15 @@ class BausteinResponse(BaseModel):
description: str description: str
purpose: str purpose: str
example: str example: str
sort_order: int = 0
created_at: str created_at: str
updated_at: str updated_at: str
class BausteinSortRequest(BaseModel):
instructions: str = Field(default="", max_length=2000)
class SuggestionResponse(BaseModel): class SuggestionResponse(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, 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 ( from models import (
GuideCreateRequest, GuideReworkRequest, GuideResponse, GuideCreateRequest, GuideReworkRequest, GuideResponse,
BausteinCreateRequest, BausteinReworkRequest, BausteinResponse, SuggestionResponse, BausteinCreateRequest, BausteinReworkRequest, BausteinSortRequest, BausteinResponse, SuggestionResponse,
) )
from paths import final_paths from paths import final_paths
@@ -167,6 +167,22 @@ async def rework_baustein_route(baustein_id: str, req: BausteinReworkRequest):
return {"ok": True} 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 --- # --- Baustein Suggestions ---
@router.get("/bausteine/suggestions", response_model=list[SuggestionResponse]) @router.get("/bausteine/suggestions", response_model=list[SuggestionResponse])

View File

@@ -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) { 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

@@ -5,6 +5,8 @@ import {
createBaustein, createBaustein,
deleteBaustein, deleteBaustein,
reworkBaustein, reworkBaustein,
sortBausteine,
fetchSortStatus,
fetchSuggestions, fetchSuggestions,
generateSuggestions, generateSuggestions,
fetchSuggestionsStatus, fetchSuggestionsStatus,
@@ -21,6 +23,9 @@ const suggestions = ref([])
const suggestionsLoading = ref(false) const suggestionsLoading = ref(false)
const newTitle = ref('') const newTitle = ref('')
const newInfo = ref('') const newInfo = ref('')
const sortInfo = ref('')
const sortingActive = ref(false)
let sortPollTimer = null
const reworkInputs = ref({}) const reworkInputs = ref({})
const reworkingIds = ref(new Set()) const reworkingIds = ref(new Set())
const reworkingSnapshots = new Map() const reworkingSnapshots = new Map()
@@ -95,6 +100,26 @@ async function handleRegenerate() {
startPolling() 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) { async function handleRework(b) {
const instructions = (reworkInputs.value[b.id] || '').trim() const instructions = (reworkInputs.value[b.id] || '').trim()
if (!instructions) return if (!instructions) return
@@ -147,6 +172,10 @@ function stopPolling() {
clearInterval(bausteinPollTimer) clearInterval(bausteinPollTimer)
bausteinPollTimer = null bausteinPollTimer = null
} }
if (sortPollTimer) {
clearInterval(sortPollTimer)
sortPollTimer = null
}
} }
async function init() { async function init() {
@@ -177,12 +206,22 @@ onUnmounted(stopPolling)
placeholder="Thema…" placeholder="Thema…"
@keyup.enter="handleAdd" @keyup.enter="handleAdd"
/> />
<textarea <input
v-model="newInfo" v-model="newInfo"
placeholder="Zusätzliche Infos (optional)…" placeholder="Zusätzliche Infos (optional)…"
rows="4" @keyup.enter="handleAdd"
></textarea> />
<button class="new-btn" @click="handleAdd" :disabled="!newTitle.trim()">Generieren</button> <button class="new-btn" @click="handleAdd" :disabled="!newTitle.trim()">Generieren</button>
<h3 class="new-card-section">Ordnen</h3>
<input
v-model="sortInfo"
placeholder="Zusätzliche Infos (optional)…"
@keyup.enter="handleSort"
/>
<button class="new-btn" @click="handleSort" :disabled="sortingActive || !bausteine.length">
{{ sortingActive ? 'Ordne…' : 'Ordnen' }}
</button>
</div> </div>
<div v-for="b in bausteine" :key="b.id" class="card"> <div v-for="b in bausteine" :key="b.id" class="card">
@@ -297,8 +336,13 @@ onUnmounted(stopPolling)
margin: 0; margin: 0;
} }
.new-card input, .new-card-section {
.new-card textarea { margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid #e2e5e9;
}
.new-card input {
width: 100%; width: 100%;
padding: 8px 10px; padding: 8px 10px;
border: 1px solid #d8dde3; border: 1px solid #d8dde3;
@@ -306,11 +350,9 @@ onUnmounted(stopPolling)
font-size: 0.85rem; font-size: 0.85rem;
font-family: inherit; font-family: inherit;
outline: none; outline: none;
resize: vertical;
} }
.new-card input:focus, .new-card input:focus {
.new-card textarea:focus {
border-color: #6366f1; border-color: #6366f1;
} }
@@ -323,7 +365,6 @@ onUnmounted(stopPolling)
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
margin-top: auto;
} }
.new-btn:disabled { .new-btn:disabled {

View File

@@ -83,6 +83,32 @@ CODE-BEISPIEL
- Mit kurzem Label oben (2-4 Wörter) - 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.)
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:
- `<` → `&lt;`
- `>` → `&gt;`
- `&` → `&amp;`
- Grund: der Code wird via v-html im Browser gerendert. Rohe `<h1>` werden sonst als echtes DOM-Element interpretiert und verschwinden.
- Gut: `<span class="t">&lt;h1&gt;</span>Text<span class="t">&lt;/h1&gt;</span>`
- Schlecht: `<span class="t"><h1></span>Text<span class="t"></h1></span>`
- Schlecht: `<h1>Text</h1>` (komplett ohne Spans und ohne Entities)
- Diese Regel gilt NUR für die Inhalte des Code-Beispiels, NICHT für die `<span class="...">`-Wrapper selbst
KONKRETES BEISPIEL — Baustein "Header" (HTML)
```json
{
"title": "Header",
"description": "Definiert eine Überschrift.",
"purpose": "Strukturiert die Seiteninhalte.",
"examples": [
{
"label": "Alle Header",
"code": "<span class=\"t\">&lt;h1&gt;</span>Hauptüberschrift<span class=\"t\">&lt;/h1&gt;</span>\n<span class=\"t\">&lt;h2&gt;</span>Kapitel<span class=\"t\">&lt;/h2&gt;</span>\n<span class=\"t\">&lt;h3&gt;</span>Unterabschnitt<span class=\"t\">&lt;/h3&gt;</span>"
}
]
}
```
VERMEIDEN VERMEIDEN
- Lange Erklärungstexte - Lange Erklärungstexte
- Mehrere Sätze für Beschreibung oder Zweck - Mehrere Sätze für Beschreibung oder Zweck
@@ -111,6 +137,7 @@ GENERIERUNG MIT FEEDBACK-LOOP
- Label über Code-Block 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? - Ist das gewählte Beispiel wirklich das typischste?
- Bei HTML/XML/JSX-Code: alle `<` und `>` als `&lt;` und `&gt;` geschrieben?
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?
``` ```