update
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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,))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
- `<` → `<`
|
||||||
|
- `>` → `>`
|
||||||
|
- `&` → `&`
|
||||||
|
- 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"><h1></span>Text<span class="t"></h1></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\"><h1></span>Hauptüberschrift<span class=\"t\"></h1></span>\n<span class=\"t\"><h2></span>Kapitel<span class=\"t\"></h2></span>\n<span class=\"t\"><h3></span>Unterabschnitt<span class=\"t\"></h3></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 `<` und `>` 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?
|
||||||
```
|
```
|
||||||
Reference in New Issue
Block a user