update
This commit is contained in:
@@ -15,6 +15,7 @@ TIMEOUTS = {
|
|||||||
"recherche": (1800, 0), # fix 30 min
|
"recherche": (1800, 0), # fix 30 min
|
||||||
"auswahl": (600, 10),
|
"auswahl": (600, 10),
|
||||||
"auswahl_check": (300, 2),
|
"auswahl_check": (300, 2),
|
||||||
|
"ergaenzung": (900, 0), # Themenfeld-Ergänzung bei Projekten (Web-Recherche)
|
||||||
"guide_auswahl": (300, 5), # pro Baustein im Inventar
|
"guide_auswahl": (300, 5), # pro Baustein im Inventar
|
||||||
"guide_check": (300, 2), # Auswahl-/Gliederungs-Prüfung (nur Titellisten)
|
"guide_check": (300, 2), # Auswahl-/Gliederungs-Prüfung (nur Titellisten)
|
||||||
"plan": (300, 5),
|
"plan": (300, 5),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
import shutil
|
import shutil
|
||||||
|
import subprocess
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
@@ -26,7 +27,7 @@ async def cancel_guide(guide_id: str) -> bool:
|
|||||||
_cancelled.add(guide_id)
|
_cancelled.add(guide_id)
|
||||||
kill_process(guide_id)
|
kill_process(guide_id)
|
||||||
now = datetime.now(timezone.utc).isoformat()
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen", updated_at=now)
|
await update_guide(guide_id, status="error", progress=None, error_msg="Abgebrochen — Fortschritt bleibt erhalten", updated_at=now)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -201,6 +202,13 @@ BAUSTEINE_STEPS = ("Recherche", "Auswahl", "Prüfung")
|
|||||||
_CATEGORIES = ("KERN", "WICHTIG", "REST") # nur noch für den Altformat-Reader
|
_CATEGORIES = ("KERN", "WICHTIG", "REST") # nur noch für den Altformat-Reader
|
||||||
|
|
||||||
|
|
||||||
|
def _bausteine_steps(topic: str) -> tuple:
|
||||||
|
"""Projekte haben einen 4. Schritt: Themenfeld-Ergänzung per Web-Recherche."""
|
||||||
|
if project_dir(topic).is_dir():
|
||||||
|
return BAUSTEINE_STEPS + ("Ergänzung",)
|
||||||
|
return BAUSTEINE_STEPS
|
||||||
|
|
||||||
|
|
||||||
def _bausteine_files(topic: str) -> dict:
|
def _bausteine_files(topic: str) -> dict:
|
||||||
arbeit = arbeit_dir(topic)
|
arbeit = arbeit_dir(topic)
|
||||||
return {
|
return {
|
||||||
@@ -209,11 +217,12 @@ def _bausteine_files(topic: str) -> dict:
|
|||||||
"recherche": [arbeit / f"recherche-{i}.md" for i in (1, 2, 3, 4)],
|
"recherche": [arbeit / f"recherche-{i}.md" for i in (1, 2, 3, 4)],
|
||||||
"auswahl": [arbeit / f"auswahl-{i}.md" for i in (1, 2)],
|
"auswahl": [arbeit / f"auswahl-{i}.md" for i in (1, 2)],
|
||||||
"auswahl_check": arbeit / "auswahl-check.json",
|
"auswahl_check": arbeit / "auswahl-check.json",
|
||||||
|
"ergaenzung": arbeit / "ergaenzung.json",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _alle_slot_dateien(files: dict) -> list[Path]:
|
def _alle_slot_dateien(files: dict) -> list[Path]:
|
||||||
return [*files["recherche"], *files["auswahl"], files["auswahl_check"]]
|
return [*files["recherche"], *files["auswahl"], files["auswahl_check"], files["ergaenzung"]]
|
||||||
|
|
||||||
|
|
||||||
def cancel_bausteine(topic: str) -> bool:
|
def cancel_bausteine(topic: str) -> bool:
|
||||||
@@ -233,10 +242,13 @@ def _resume_step(topic: str) -> int:
|
|||||||
return 1
|
return 1
|
||||||
if not files["auswahl_check"].exists():
|
if not files["auswahl_check"].exists():
|
||||||
return 2
|
return 2
|
||||||
|
if project_dir(topic).is_dir() and not files["ergaenzung"].exists():
|
||||||
return 3
|
return 3
|
||||||
|
return len(_bausteine_steps(topic))
|
||||||
|
|
||||||
|
|
||||||
def bausteine_status(topic: str) -> dict:
|
def bausteine_status(topic: str) -> dict:
|
||||||
|
steps = _bausteine_steps(topic)
|
||||||
ready = bausteine_path(topic).exists()
|
ready = bausteine_path(topic).exists()
|
||||||
generating = topic in _bausteine_progress
|
generating = topic in _bausteine_progress
|
||||||
partial = False
|
partial = False
|
||||||
@@ -244,21 +256,21 @@ def bausteine_status(topic: str) -> dict:
|
|||||||
current = _bausteine_step.get(topic)
|
current = _bausteine_step.get(topic)
|
||||||
states = [
|
states = [
|
||||||
"pending" if current is None else "done" if i < current else "active" if i == current else "pending"
|
"pending" if current is None else "done" if i < current else "active" if i == current else "pending"
|
||||||
for i in range(len(BAUSTEINE_STEPS))
|
for i in range(len(steps))
|
||||||
]
|
]
|
||||||
elif ready:
|
elif ready:
|
||||||
states = ["done"] * len(BAUSTEINE_STEPS)
|
states = ["done"] * len(steps)
|
||||||
else:
|
else:
|
||||||
nxt = _resume_step(topic)
|
nxt = _resume_step(topic)
|
||||||
partial = nxt > 0
|
partial = nxt > 0
|
||||||
states = ["done" if i < nxt else "pending" for i in range(len(BAUSTEINE_STEPS))]
|
states = ["done" if i < nxt else "pending" for i in range(len(steps))]
|
||||||
return {
|
return {
|
||||||
"ready": ready,
|
"ready": ready,
|
||||||
"generating": generating,
|
"generating": generating,
|
||||||
"progress": _bausteine_progress.get(topic),
|
"progress": _bausteine_progress.get(topic),
|
||||||
"error": _bausteine_errors.get(topic),
|
"error": _bausteine_errors.get(topic),
|
||||||
"partial": partial,
|
"partial": partial,
|
||||||
"steps": [{"label": label, "state": s} for label, s in zip(BAUSTEINE_STEPS, states)],
|
"steps": [{"label": label, "state": s} for label, s in zip(steps, states)],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -273,6 +285,41 @@ def reset_bausteine(topic: str) -> None:
|
|||||||
_bausteine_errors.pop(topic, None)
|
_bausteine_errors.pop(topic, None)
|
||||||
|
|
||||||
|
|
||||||
|
def _ergaenzung_schema(data):
|
||||||
|
"""{"bausteine": [{"titel", "beschreibung"}]} → Liste (leer erlaubt) · sonst None."""
|
||||||
|
if not isinstance(data, dict) or not isinstance(data.get("bausteine"), list):
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for b in data["bausteine"]:
|
||||||
|
if not isinstance(b, dict) or not isinstance(b.get("titel"), str) or not isinstance(b.get("beschreibung"), str):
|
||||||
|
return None
|
||||||
|
titel, beschreibung = b["titel"].strip(), b["beschreibung"].strip()
|
||||||
|
if not titel:
|
||||||
|
return None
|
||||||
|
out.append((titel, beschreibung))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _pdfs_konvertieren(project: Path) -> None:
|
||||||
|
"""PDFs im Projekt in .txt wandeln (pdftotext) — Agenten lesen Text statt Seiten-Bildern.
|
||||||
|
|
||||||
|
Wird vor jeder Projekt-Generierung aufgerufen; konvertiert nur, wenn die
|
||||||
|
.txt fehlt oder älter als das PDF ist. Das Original bleibt unangetastet.
|
||||||
|
"""
|
||||||
|
if shutil.which("pdftotext") is None:
|
||||||
|
_log(project.name, "pdftotext nicht installiert — PDFs bleiben unkonvertiert")
|
||||||
|
return
|
||||||
|
for pdf in project.rglob("*.pdf"):
|
||||||
|
txt = pdf.with_suffix(".txt")
|
||||||
|
if txt.exists() and txt.stat().st_mtime >= pdf.stat().st_mtime:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
subprocess.run(["pdftotext", "-layout", str(pdf), str(txt)], check=True, timeout=120)
|
||||||
|
_log(project.name, f"PDF konvertiert: {pdf.name} → {txt.name}")
|
||||||
|
except Exception as e:
|
||||||
|
_log(project.name, f"PDF-Konvertierung fehlgeschlagen ({pdf.name}): {e}")
|
||||||
|
|
||||||
|
|
||||||
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str = "", project: Path | None = None) -> str:
|
def _build_recherche_prompt(topic: str, out_path: Path, instructions: str = "", project: Path | None = None) -> str:
|
||||||
if project:
|
if project:
|
||||||
source = _prompt("Bausteine-Quelle-Projekt", project=project)
|
source = _prompt("Bausteine-Quelle-Projekt", project=project)
|
||||||
@@ -384,6 +431,8 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
try:
|
try:
|
||||||
async with _semaphore:
|
async with _semaphore:
|
||||||
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
files["arbeit"].mkdir(parents=True, exist_ok=True)
|
||||||
|
if project:
|
||||||
|
await asyncio.to_thread(_pdfs_konvertieren, project)
|
||||||
# „Neu erstellen": fertige Bausteine → kompletter Frischstart.
|
# „Neu erstellen": fertige Bausteine → kompletter Frischstart.
|
||||||
# Sonst sind Slot-Dateien Reste eines Abbruchs/Fehlers → Resume.
|
# Sonst sind Slot-Dateien Reste eines Abbruchs/Fehlers → Resume.
|
||||||
if final_path.exists():
|
if final_path.exists():
|
||||||
@@ -486,6 +535,40 @@ async def generate_bausteine(topic: str, instructions: str = "", provider: str =
|
|||||||
texts = [t for _, t in sorted(entries.items())] + list(patch["nachtraege"])
|
texts = [t for _, t in sorted(entries.items())] + list(patch["nachtraege"])
|
||||||
entries = {i: t for i, t in enumerate(texts, 1)}
|
entries = {i: t for i, t in enumerate(texts, 1)}
|
||||||
|
|
||||||
|
# Schritt 4 (nur Projekte): Themenfeld-Ergänzung — Skript/Projekt ist ein Ausschnitt,
|
||||||
|
# ein Web-Agent ergänzt kanonisch fehlende Bausteine, markiert mit [Ergänzung].
|
||||||
|
if project:
|
||||||
|
set_p("Ergänze Themenfeld…", step=3)
|
||||||
|
erg_path = files["ergaenzung"]
|
||||||
|
ergaenzungen = _ergaenzung_schema(_json_datei(erg_path))
|
||||||
|
if ergaenzungen is None:
|
||||||
|
erg_path.unlink(missing_ok=True)
|
||||||
|
slots = [{
|
||||||
|
"key": f"bausteine-{topic}-ergaenzung-1",
|
||||||
|
"prompt": _prompt(
|
||||||
|
"Bausteine-Ergaenzung",
|
||||||
|
topic=topic, bausteine="\n".join(f"- {t}" for t in entries.values()),
|
||||||
|
out_path=erg_path, extra=_extra(instructions),
|
||||||
|
),
|
||||||
|
"role": "quick", "capabilities": "full",
|
||||||
|
"payload": (lambda result: _ergaenzung_schema(_json_datei(erg_path))),
|
||||||
|
}]
|
||||||
|
res = await _race(topic, "Ergänzung", slots, 1, _timeout("ergaenzung"), provider, cancelled=is_cancelled)
|
||||||
|
if is_cancelled():
|
||||||
|
abgebrochen()
|
||||||
|
return
|
||||||
|
if res is None:
|
||||||
|
_bausteine_errors[topic] = "Ergänzung fehlgeschlagen (kein gültiges Ergebnis)"
|
||||||
|
return
|
||||||
|
ergaenzungen = res[0]
|
||||||
|
idx = _titel_index(entries)
|
||||||
|
neu = [(t, b) for t, b in ergaenzungen if _titel_aufloesen(idx, t) is None]
|
||||||
|
if neu:
|
||||||
|
_log(topic, f"Ergänzung: {len(neu)} Baustein(e) aus dem Themenfeld ergänzt")
|
||||||
|
start = max(entries, default=0) + 1
|
||||||
|
for off, (t, b) in enumerate(neu):
|
||||||
|
entries[start + off] = f"{t} — {b} [Ergänzung]"
|
||||||
|
|
||||||
# Titel eindeutig machen und unsortiertes Inventar schreiben
|
# Titel eindeutig machen und unsortiertes Inventar schreiben
|
||||||
entries = _eindeutige_titel(entries)
|
entries = _eindeutige_titel(entries)
|
||||||
final_path.write_text(
|
final_path.write_text(
|
||||||
@@ -1192,6 +1275,9 @@ async def generate_guide(guide_id: str, topic: str, format_name: str, instructio
|
|||||||
if guide_id in _cancelled:
|
if guide_id in _cancelled:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if project:
|
||||||
|
await asyncio.to_thread(_pdfs_konvertieren, project)
|
||||||
|
|
||||||
# „Neu erstellen": fertiger Guide → kompletter Frischstart.
|
# „Neu erstellen": fertiger Guide → kompletter Frischstart.
|
||||||
# Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume.
|
# Sonst sind Schritt-Dateien Reste eines Abbruchs/Fehlers → Resume.
|
||||||
if content_path.exists():
|
if content_path.exists():
|
||||||
|
|||||||
@@ -169,8 +169,10 @@ async def create(req: GuideCreateRequest):
|
|||||||
for g in await list_guides():
|
for g in await list_guides():
|
||||||
if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"):
|
if g["topic"] == req.topic.strip() and g["format"] == req.format and g["status"] in ("queued", "generating"):
|
||||||
raise HTTPException(409, "Generierung läuft bereits")
|
raise HTTPException(409, "Generierung läuft bereits")
|
||||||
# Lernschulden-Regel: neue Guides nur, wenn das Format weniger als 5 offene hat (erstellt, nicht absolviert)
|
# Lernschulden-Regel: neue Guides nur, wenn das Format weniger als 5 offene hat (erstellt, nicht absolviert).
|
||||||
if req.format != "OnePager" and not guide_content_path(req.topic.strip(), req.format).exists():
|
# Resume (Schritt-Dateien vorhanden) ist ausgenommen — der Guide wurde bereits angefangen.
|
||||||
|
content = guide_content_path(req.topic.strip(), req.format)
|
||||||
|
if req.format != "OnePager" and not content.exists() and not guide_slot_dateien(content):
|
||||||
stat = (await _formate_stats()).get(req.format, {"erstellt": 0, "absolviert": 0})
|
stat = (await _formate_stats()).get(req.format, {"erstellt": 0, "absolviert": 0})
|
||||||
offen = stat["erstellt"] - stat["absolviert"]
|
offen = stat["erstellt"] - stat["absolviert"]
|
||||||
if offen >= MAX_OFFENE_GUIDES:
|
if offen >= MAX_OFFENE_GUIDES:
|
||||||
@@ -240,16 +242,22 @@ async def cancel(guide_id: str):
|
|||||||
|
|
||||||
|
|
||||||
@router.delete("/guides/{guide_id}")
|
@router.delete("/guides/{guide_id}")
|
||||||
async def remove(guide_id: str):
|
async def remove(guide_id: str, slots: bool = False):
|
||||||
guide = await get_guide(guide_id)
|
guide = await get_guide(guide_id)
|
||||||
if guide is None:
|
if guide is None:
|
||||||
raise HTTPException(404, "Guide nicht gefunden")
|
raise HTTPException(404, "Guide nicht gefunden")
|
||||||
|
await delete_progress(guide_id)
|
||||||
|
await delete_guide(guide_id)
|
||||||
|
# Content-/Schritt-Dateien teilen sich alle Läufe eines Thema+Formats — erst löschen,
|
||||||
|
# wenn kein Eintrag sie mehr braucht. Teilfortschritt (Schritt-Dateien ohne fertigen
|
||||||
|
# Content) bleibt fürs Resume erhalten, außer es wird explizit verlangt (slots=1).
|
||||||
|
rest = [g for g in await list_guides() if g["topic"] == guide["topic"] and g["format"] == guide["format"]]
|
||||||
|
if not rest:
|
||||||
content = guide_content_path(guide["topic"], guide["format"])
|
content = guide_content_path(guide["topic"], guide["format"])
|
||||||
|
if slots or content.exists():
|
||||||
for p in guide_slot_dateien(content):
|
for p in guide_slot_dateien(content):
|
||||||
p.unlink(missing_ok=True)
|
p.unlink(missing_ok=True)
|
||||||
content.unlink(missing_ok=True)
|
content.unlink(missing_ok=True)
|
||||||
await delete_progress(guide_id)
|
|
||||||
await delete_guide(guide_id)
|
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats } from './api.js'
|
import { fetchGuides, fetchTopics, createTopic as apiCreateTopic, deleteTopic as apiDeleteTopic, createGuide as apiCreate, deleteGuide, cancelGuide as apiCancel, fetchBausteineStatus, fetchActiveBausteine, createBausteine as apiCreateBausteine, cancelBausteine as apiCancelBausteine, deleteBausteine as apiDeleteBausteine, fetchProjects, deleteProject as apiDeleteProject, fetchProviders, fetchStats } from './api.js'
|
||||||
import TopicSidebar from './components/TopicSidebar.vue'
|
import TopicSidebar from './components/TopicSidebar.vue'
|
||||||
import TopicDetail from './components/TopicDetail.vue'
|
import TopicDetail from './components/TopicDetail.vue'
|
||||||
@@ -118,9 +118,22 @@ async function loadTopics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fehlermeldungen verhalten sich wie Flash-Messages: × blendet aus,
|
||||||
|
// beim Reload sind Alt-Fehler von vornherein ausgeblendet.
|
||||||
|
const dismissedErrors = ref(new Set())
|
||||||
|
let errorsInitialized = false
|
||||||
|
|
||||||
|
function handleDismissError(guideId) {
|
||||||
|
dismissedErrors.value = new Set([...dismissedErrors.value, guideId])
|
||||||
|
}
|
||||||
|
|
||||||
async function loadGuides() {
|
async function loadGuides() {
|
||||||
try {
|
try {
|
||||||
guides.value = await fetchGuides()
|
guides.value = await fetchGuides()
|
||||||
|
if (!errorsInitialized) {
|
||||||
|
errorsInitialized = true
|
||||||
|
dismissedErrors.value = new Set(guides.value.filter((g) => g.status === 'error').map((g) => g.id))
|
||||||
|
}
|
||||||
loadStats()
|
loadStats()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Laden:', e)
|
console.error('Fehler beim Laden:', e)
|
||||||
@@ -166,10 +179,16 @@ function selectTopic(topic) {
|
|||||||
selectedTopic.value = topic
|
selectedTopic.value = topic
|
||||||
previewGuide.value = null
|
previewGuide.value = null
|
||||||
sidebarSticky.value = false
|
sidebarSticky.value = false
|
||||||
|
localStorage.setItem('lastTopic', topic)
|
||||||
loadBausteine()
|
loadBausteine()
|
||||||
nextTick(autoPreview)
|
nextTick(autoPreview)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Beim Reload dort landen, wo man vorher war (Thema + Format)
|
||||||
|
watch(previewGuide, (g) => {
|
||||||
|
if (g) localStorage.setItem('lastFormat', g.format)
|
||||||
|
})
|
||||||
|
|
||||||
async function createTopic(topic) {
|
async function createTopic(topic) {
|
||||||
await apiCreateTopic(topic)
|
await apiCreateTopic(topic)
|
||||||
await loadTopics()
|
await loadTopics()
|
||||||
@@ -223,8 +242,8 @@ function handlePreview(guide) {
|
|||||||
previewGuide.value = guide
|
previewGuide.value = guide
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteGuide(guideId) {
|
async function handleDeleteGuide(guideId, slots = false) {
|
||||||
await deleteGuide(guideId)
|
await deleteGuide(guideId, slots)
|
||||||
if (previewGuide.value?.id === guideId) {
|
if (previewGuide.value?.id === guideId) {
|
||||||
previewGuide.value = null
|
previewGuide.value = null
|
||||||
}
|
}
|
||||||
@@ -278,7 +297,14 @@ function onVisibility() {
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()])
|
await Promise.all([loadGuides(), loadTopics(), loadProjects(), loadProviders()])
|
||||||
if (!selectedTopic.value && topics.value.length) {
|
const savedTopic = localStorage.getItem('lastTopic')
|
||||||
|
const savedFormat = localStorage.getItem('lastFormat')
|
||||||
|
if (savedTopic && [...topics.value, ...projectNames.value].includes(savedTopic)) {
|
||||||
|
selectTopic(savedTopic)
|
||||||
|
await nextTick()
|
||||||
|
const g = doneByFormat.value[savedFormat]
|
||||||
|
if (g) previewGuide.value = g
|
||||||
|
} else if (!selectedTopic.value && topics.value.length) {
|
||||||
selectTopic(topics.value[0])
|
selectTopic(topics.value[0])
|
||||||
}
|
}
|
||||||
document.addEventListener('visibilitychange', onVisibility)
|
document.addEventListener('visibilitychange', onVisibility)
|
||||||
@@ -302,6 +328,7 @@ onUnmounted(() => {
|
|||||||
:doneByFormat="doneByFormat"
|
:doneByFormat="doneByFormat"
|
||||||
:latestByFormat="latestByFormat"
|
:latestByFormat="latestByFormat"
|
||||||
:allGuides="guides"
|
:allGuides="guides"
|
||||||
|
:dismissedErrors="dismissedErrors"
|
||||||
:bausteine="bausteine"
|
:bausteine="bausteine"
|
||||||
:activeBausteine="activeBausteine"
|
:activeBausteine="activeBausteine"
|
||||||
:pinned="sidebarPinned"
|
:pinned="sidebarPinned"
|
||||||
@@ -320,6 +347,7 @@ onUnmounted(() => {
|
|||||||
@deleteProject="handleDeleteProject"
|
@deleteProject="handleDeleteProject"
|
||||||
@cancelGuide="handleCancel"
|
@cancelGuide="handleCancel"
|
||||||
@deleteGuide="handleDeleteGuide"
|
@deleteGuide="handleDeleteGuide"
|
||||||
|
@dismissError="handleDismissError"
|
||||||
@preview="handlePreview"
|
@preview="handlePreview"
|
||||||
@togglePin="toggleSidebarPin"
|
@togglePin="toggleSidebarPin"
|
||||||
@sidebarLeave="onSidebarLeave"
|
@sidebarLeave="onSidebarLeave"
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ export async function cancelGuide(id) {
|
|||||||
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
|
await fetch(`${BASE}/guides/${id}/cancel`, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteGuide(id) {
|
export async function deleteGuide(id, slots = false) {
|
||||||
await fetch(`${BASE}/guides/${id}`, { method: 'DELETE' })
|
await fetch(`${BASE}/guides/${id}${slots ? '?slots=1' : ''}`, { method: 'DELETE' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchGuideContent(id) {
|
export async function fetchGuideContent(id) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ const CH_COLORS = ['#3b82f6', '#8b5cf6', '#14b8a6', '#f59e0b', '#22c55e', '#6366
|
|||||||
|
|
||||||
// --- Inhalt laden ---
|
// --- Inhalt laden ---
|
||||||
const content = ref(null)
|
const content = ref(null)
|
||||||
|
const loadError = ref(null)
|
||||||
const doneChapters = ref(new Set())
|
const doneChapters = ref(new Set())
|
||||||
const scrollEl = ref(null)
|
const scrollEl = ref(null)
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ watch(() => props.previewGuide?.id, loadContent, { immediate: true })
|
|||||||
|
|
||||||
async function loadContent() {
|
async function loadContent() {
|
||||||
content.value = null
|
content.value = null
|
||||||
|
loadError.value = null
|
||||||
doneChapters.value = new Set()
|
doneChapters.value = new Set()
|
||||||
const g = props.previewGuide
|
const g = props.previewGuide
|
||||||
if (!g || g.status !== 'done') return
|
if (!g || g.status !== 'done') return
|
||||||
@@ -62,6 +64,7 @@ async function loadContent() {
|
|||||||
content.value = await fetchGuideContent(g.id)
|
content.value = await fetchGuideContent(g.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Fehler beim Laden des Guides:', e)
|
console.error('Fehler beim Laden des Guides:', e)
|
||||||
|
loadError.value = 'Inhalt nicht verfügbar — die Datei fehlt. Guide neu generieren (▶).'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -236,7 +239,7 @@ async function send() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="previewGuide" class="empty-preview">
|
<div v-else-if="previewGuide" class="empty-preview">
|
||||||
<p>Lade Inhalt…</p>
|
<p>{{ loadError || 'Lade Inhalt…' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="empty-preview" v-else>
|
<div class="empty-preview" v-else>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const props = defineProps({
|
|||||||
doneByFormat: { type: Object, default: () => ({}) },
|
doneByFormat: { type: Object, default: () => ({}) },
|
||||||
latestByFormat: { type: Object, default: () => ({}) },
|
latestByFormat: { type: Object, default: () => ({}) },
|
||||||
allGuides: { type: Array, default: () => [] },
|
allGuides: { type: Array, default: () => [] },
|
||||||
|
dismissedErrors: { type: Object, default: () => new Set() },
|
||||||
bausteine: { type: Object, default: () => ({ ready: false, generating: false, progress: null, error: null }) },
|
bausteine: { type: Object, default: () => ({ ready: false, generating: false, progress: null, error: null }) },
|
||||||
activeBausteine: { type: Array, default: () => [] },
|
activeBausteine: { type: Array, default: () => [] },
|
||||||
pinned: { type: Boolean, default: true },
|
pinned: { type: Boolean, default: true },
|
||||||
@@ -17,7 +18,7 @@ const props = defineProps({
|
|||||||
providers: { type: Array, default: () => [] },
|
providers: { type: Array, default: () => [] },
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
|
const emit = defineEmits(['select', 'create', 'formatClick', 'bausteineClick', 'cancelBausteine', 'resetBausteine', 'deleteTopic', 'deleteProject', 'cancelGuide', 'deleteGuide', 'dismissError', 'preview', 'togglePin', 'sidebarLeave', 'toggleDark', 'setProvider'])
|
||||||
|
|
||||||
function providerAvailable(id) {
|
function providerAvailable(id) {
|
||||||
const p = props.providers.find((x) => x.id === id)
|
const p = props.providers.find((x) => x.id === id)
|
||||||
@@ -104,21 +105,38 @@ function guideStatus(format) {
|
|||||||
const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung']
|
const GUIDE_STEPS = ['Auswahl', 'Auswahl-Prüfung', 'Gliederung', 'Gliederungs-Prüfung', 'Schreiben', 'Lese-Prüfung']
|
||||||
const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung']
|
const ONEPAGER_STEPS = ['Recherche', 'Recherche-Prüfung', 'Bauen', 'Prüfung']
|
||||||
|
|
||||||
|
// Kugeln werden wie bei den Bausteinen immer angezeigt:
|
||||||
|
// fertig = alle grün, laufend = live, abgebrochen = Teilfortschritt, sonst grau
|
||||||
function guideSteps(format) {
|
function guideSteps(format) {
|
||||||
const st = guideStatus(format)
|
|
||||||
if (st !== 'generating' && st !== 'queued') return []
|
|
||||||
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
|
const labels = format === 'OnePager' ? ONEPAGER_STEPS : GUIDE_STEPS
|
||||||
|
const st = guideStatus(format)
|
||||||
|
if (st === 'generating' || st === 'queued') {
|
||||||
const step = props.latestByFormat[format]?.step ?? -1
|
const step = props.latestByFormat[format]?.step ?? -1
|
||||||
return labels.map((label, i) => ({
|
return labels.map((label, i) => ({
|
||||||
label,
|
label,
|
||||||
state: i < step ? 'done' : i === step ? 'active' : 'pending',
|
state: i < step ? 'done' : i === step ? 'active' : 'pending',
|
||||||
}))
|
}))
|
||||||
|
}
|
||||||
|
if (props.doneByFormat[format]) {
|
||||||
|
return labels.map((label) => ({ label, state: 'done' }))
|
||||||
|
}
|
||||||
|
if (abgebrochen(format)) {
|
||||||
|
const step = props.latestByFormat[format]?.step ?? 0
|
||||||
|
return labels.map((label, i) => ({ label, state: i < step ? 'done' : 'pending' }))
|
||||||
|
}
|
||||||
|
return labels.map((label) => ({ label, state: 'pending' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorMsg(format) {
|
function errorMsg(format) {
|
||||||
const latest = props.latestByFormat[format]
|
const latest = props.latestByFormat[format]
|
||||||
if (latest?.status === 'error') return latest.error_msg || 'Fehler bei der Generierung'
|
if (latest?.status !== 'error' || props.dismissedErrors.has(latest.id)) return ''
|
||||||
return ''
|
return latest.error_msg || 'Fehler bei der Generierung'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abgebrochener Lauf = Teilfortschritt vorhanden: ▶ setzt fort, ✕ löscht den Fortschritt
|
||||||
|
function abgebrochen(format) {
|
||||||
|
const latest = props.latestByFormat[format]
|
||||||
|
return latest?.status === 'error' && (latest.error_msg || '').startsWith('Abgebrochen')
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFormatClick(format) {
|
function handleFormatClick(format) {
|
||||||
@@ -150,11 +168,10 @@ function handlePlay(format) {
|
|||||||
emit('formatClick', { format, instructions: '' })
|
emit('formatClick', { format, instructions: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Flash-Message-Verhalten: × blendet nur aus, nichts wird gelöscht
|
||||||
function dismissError(format) {
|
function dismissError(format) {
|
||||||
const latest = props.latestByFormat[format]
|
const latest = props.latestByFormat[format]
|
||||||
if (latest?.status === 'error') {
|
if (latest?.status === 'error') emit('dismissError', latest.id)
|
||||||
emit('deleteGuide', latest.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDelete(format) {
|
function handleDelete(format) {
|
||||||
@@ -297,7 +314,7 @@ function confirmDeleteProject(name) {
|
|||||||
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
|
<template v-if="guideStatus(f.key) !== 'generating' && guideStatus(f.key) !== 'queued'">
|
||||||
<button
|
<button
|
||||||
class="action-btn play"
|
class="action-btn play"
|
||||||
:title="playLock(f.key) || 'Generieren'"
|
:title="playLock(f.key) || (abgebrochen(f.key) ? 'Fortsetzen' : 'Generieren')"
|
||||||
:disabled="!!playLock(f.key)"
|
:disabled="!!playLock(f.key)"
|
||||||
@click="handlePlay(f.key)"
|
@click="handlePlay(f.key)"
|
||||||
>▶</button>
|
>▶</button>
|
||||||
@@ -306,7 +323,7 @@ function confirmDeleteProject(name) {
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="errorMsg(f.key)" class="format-error">
|
<div v-if="errorMsg(f.key)" class="format-error">
|
||||||
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
<span class="format-error-text">{{ errorMsg(f.key) }}</span>
|
||||||
<button class="format-error-x" title="Fehler entfernen" @click="dismissError(f.key)">×</button>
|
<button class="format-error-x" title="Ausblenden" @click="dismissError(f.key)">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -664,6 +681,7 @@ function confirmDeleteProject(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.format-x.armed,
|
.format-x.armed,
|
||||||
|
.format-error-x.armed,
|
||||||
.delete-topic.armed {
|
.delete-topic.armed {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
|
|||||||
21
templates/Prompt/Bausteine-Ergaenzung.md
Normal file
21
templates/Prompt/Bausteine-Ergaenzung.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
Prüfe das Baustein-Inventar zum Thema "{topic}" auf Vollständigkeit gegenüber dem Themenfeld.
|
||||||
|
|
||||||
|
Das Inventar stammt aus einem Projekt/Skript — es kann Bausteine geben, die fachlich zum Thema gehören, dort aber nicht behandelt werden. Finde genau diese Lücken.
|
||||||
|
|
||||||
|
VORHANDENE BAUSTEINE:
|
||||||
|
{bausteine}
|
||||||
|
|
||||||
|
Regeln:
|
||||||
|
- Recherchiere das Themenfeld (Lehrbücher, Standardreferenzen) und ergänze NUR Bausteine, die kanonisch dazugehören und im Inventar fehlen.
|
||||||
|
- Ein Baustein löst GENAU EIN PROBLEM und ist ATOMAR — gleiche Maßstäbe wie im Inventar.
|
||||||
|
- KEINE Varianten, Umformulierungen oder Vertiefungen vorhandener Bausteine — nur echte Lücken.
|
||||||
|
- Erfinde nichts: nur Bausteine, die du in der Recherche belegt hast.
|
||||||
|
- Titel und Beschreibung auf DEUTSCH (Fachbegriffe bleiben original), Beschreibung maximal ~12 Wörter.
|
||||||
|
- Gibt es keine Lücken, liefere eine leere Liste — das ist ein gültiges Ergebnis.
|
||||||
|
|
||||||
|
Schreibe NUR die JSON-Datei nach: {out_path}
|
||||||
|
|
||||||
|
Format:
|
||||||
|
{{"bausteine": [{{"titel": "…", "beschreibung": "…"}}]}}
|
||||||
|
Keine Lücken: {{"bausteine": []}}
|
||||||
|
{extra}
|
||||||
@@ -1 +1 @@
|
|||||||
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku-Ordner und den relevanten Quellcode mit dem Read-Tool. Die Bausteine müssen das echte Projekt widerspiegeln, nichts Erfundenes.
|
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku-Ordner und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. Die Bausteine müssen das echte Projekt widerspiegeln, nichts Erfundenes.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Die Fakten stammen aus dem Projekt unter {project} — lies bei Bedarf Dateien mit Read/Bash nach.
|
Die Fakten stammen aus dem Projekt unter {project} — lies bei Bedarf Dateien mit Read/Bash nach. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF.
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ FAKTENBASIS (alleinige Quelle, nichts hinzuerfinden):
|
|||||||
|
|
||||||
Erstelle GENAU diese 7 Karten (JSON-Schlüssel exakt so):
|
Erstelle GENAU diese 7 Karten (JSON-Schlüssel exakt so):
|
||||||
- "info" — Titel: "{topic}". Kurzbeschreibung in 1–2 Sätzen, darunter technische Daten als Stichpunkte (Art/Typ, Version/Stand, Lizenz/Kosten).
|
- "info" — Titel: "{topic}". Kurzbeschreibung in 1–2 Sätzen, darunter technische Daten als Stichpunkte (Art/Typ, Version/Stand, Lizenz/Kosten).
|
||||||
- "eigenschaften" — Titel: "Kerneigenschaften". Die 4–7 prägenden Eigenschaften des Systems als Stichpunkte.
|
- "eigenschaften" — Titel: "Kerneigenschaften". Was einen IM Thema erwartet: kleine Übersicht der Inhalte/Teilgebiete.
|
||||||
- "beispiel" — Titel: "Beispiel". EIN anschauliches, typisches Codebeispiel (Markdown-Codeblock) mit einem Satz Erklärung.
|
- "beispiel" — Titel: "Beispiel". EIN anschauliches, typisches Codebeispiel (Markdown-Codeblock) mit einem Satz Erklärung.
|
||||||
- "zusammenhaenge" — Titel: "Zusammenhänge". Mit welchen Systemen/Themen es zusammenhängt — Stichpunkte mit je einem halben Satz.
|
- "zusammenhaenge" — Titel: "Zusammenhänge". Mit welchen ANDEREN Themen es zusammenhängt — Nachbarthemen außerhalb dieses Themas, keine Inhalte des Themas selbst.
|
||||||
- "voraussetzungen" — Titel: "Voraussetzungen". Was man vorher können oder haben muss.
|
- "voraussetzungen" — Titel: "Voraussetzungen". Welche Themen man vorher bearbeitet haben sollte, um hier klarzukommen.
|
||||||
- "modern" — Titel: "Moderne Features". Was aktuell ist und heute verwendet wird.
|
- "modern" — Titel: "Moderne Features". NUR was in den letzten Jahren neu dazugekommen ist. Gibt es nichts Neues: ehrlich "Keine." mit einem Satz Begründung.
|
||||||
- "veraltet" — Titel: "Veraltete Features". Was es noch gibt, aber nicht mehr verwendet werden sollte. Gibt es nichts Veraltetes: ehrlich "Keine." mit einem Satz Begründung — nichts erfinden.
|
- "veraltet" — Titel: "Veraltete Features". Was nicht mehr verwendet wird. Gibt es nichts Veraltetes: ehrlich "Keine." mit einem Satz Begründung — nichts erfinden.
|
||||||
|
|
||||||
KOMPAKTHEIT — der OnePager muss OHNE Scrollen auf eine Bildschirmseite passen:
|
KOMPAKTHEIT — der OnePager muss OHNE Scrollen auf eine Bildschirmseite passen:
|
||||||
- Maximal 5 Stichpunkte pro Karte, je maximal ~8 Wörter (Schlagwort + halber Satz).
|
- Maximal 5 Stichpunkte pro Karte, je maximal ~8 Wörter (Schlagwort + halber Satz).
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku und den relevanten Quellcode mit dem Read-Tool. Erfasse Zweck, Architektur und die wichtigsten Konzepte — nichts Erfundenes.
|
Das Thema ist das Projekt unter {project}. Verschaffe dir mit Bash (ls/find) einen Überblick und lies README, Doku und den relevanten Quellcode mit dem Read-Tool. PDFs liegen als gleichnamige .txt-Dateien vor — lies IMMER die .txt, nie das PDF. Erfasse Zweck, Architektur und die wichtigsten Konzepte — nichts Erfundenes.
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ FAKTENBASIS:
|
|||||||
Sie muss diese Dimensionen abdecken:
|
Sie muss diese Dimensionen abdecken:
|
||||||
1. Kurzbeschreibung (Art des Projekts, Gegenstand)
|
1. Kurzbeschreibung (Art des Projekts, Gegenstand)
|
||||||
2. Technische Daten (Technologie/Format, Umfang, Stand/Aktualität)
|
2. Technische Daten (Technologie/Format, Umfang, Stand/Aktualität)
|
||||||
3. Kerneigenschaften (prägende Konzepte, Komponenten oder Inhalte)
|
3. Inhaltsübersicht (was einen im Projekt erwartet)
|
||||||
4. Ein typisches Beispiel aus dem Projekt
|
4. Ein typisches Beispiel aus dem Projekt
|
||||||
5. Zusammenhänge (Umfeld, Abhängigkeiten, angrenzende Systeme/Themen)
|
5. Zusammenhänge mit ANDEREN Themen (Nachbarthemen außerhalb des Projektinhalts)
|
||||||
6. Voraussetzungen
|
6. Voraussetzungen (vorher zu bearbeitende Themen)
|
||||||
7. Moderne vs. veraltete Teile (oder die ausdrückliche Feststellung, dass nichts veraltet ist)
|
7. Neuerungen der letzten Jahre vs. nicht mehr Verwendetes (oder die ausdrückliche Feststellung, dass es jeweils nichts gibt)
|
||||||
|
|
||||||
Prüfe:
|
Prüfe:
|
||||||
1. Ist jede Dimension mit konkreten Fakten aus den Projektdateien belegt (Namen, Zahlen — nicht vage)?
|
1. Ist jede Dimension mit konkreten Fakten aus den Projektdateien belegt (Namen, Zahlen — nicht vage)?
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ FAKTENBASIS:
|
|||||||
Sie muss diese Dimensionen abdecken:
|
Sie muss diese Dimensionen abdecken:
|
||||||
1. Kurzbeschreibung (1–2 Sätze)
|
1. Kurzbeschreibung (1–2 Sätze)
|
||||||
2. Technische Daten (Art/Typ, Version/Stand, Lizenz/Kosten)
|
2. Technische Daten (Art/Typ, Version/Stand, Lizenz/Kosten)
|
||||||
3. Kerneigenschaften des Systems
|
3. Inhaltsübersicht (was einen im Thema erwartet)
|
||||||
4. Ein typisches Beispiel
|
4. Ein typisches Beispiel
|
||||||
5. Zusammenhänge mit anderen Systemen/Themen
|
5. Zusammenhänge mit ANDEREN Themen (Nachbarthemen, nicht Inhalte des Themas selbst)
|
||||||
6. Voraussetzungen
|
6. Voraussetzungen (vorher zu bearbeitende Themen)
|
||||||
7. Moderne vs. veraltete Features (oder die ausdrückliche Feststellung, dass nichts veraltet ist)
|
7. Neuerungen der letzten Jahre vs. nicht mehr Verwendetes (oder die ausdrückliche Feststellung, dass es jeweils nichts gibt)
|
||||||
|
|
||||||
Prüfe:
|
Prüfe:
|
||||||
1. Ist jede Dimension mit konkreten Fakten belegt (Namen, Versionen, Zahlen — nicht vage)?
|
1. Ist jede Dimension mit konkreten Fakten belegt (Namen, Versionen, Zahlen — nicht vage)?
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Se
|
|||||||
Erfasse gezielt diese Dimensionen:
|
Erfasse gezielt diese Dimensionen:
|
||||||
1. Kurzbeschreibung: Was ist "{topic}" in 1–2 Sätzen (Art des Projekts, Gegenstand)?
|
1. Kurzbeschreibung: Was ist "{topic}" in 1–2 Sätzen (Art des Projekts, Gegenstand)?
|
||||||
2. Technische Daten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
|
2. Technische Daten: Technologie/Format, Umfang (Dateien/Seiten/Module), Stand/Aktualität.
|
||||||
3. Kerneigenschaften: die prägenden Konzepte, Komponenten oder Inhalte des Projekts.
|
3. Inhaltsübersicht: Was erwartet einen — die wichtigsten Inhalte/Teilgebiete des Projekts.
|
||||||
4. Beispiel: ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
|
4. Beispiel: ein typisches, konkretes Beispiel aus dem Projekt (zentraler Code-Flow bzw. Kerninhalt).
|
||||||
5. Zusammenhänge: in welchem Umfeld es steht (Abhängigkeiten, angrenzende Systeme/Themen).
|
5. Zusammenhänge: mit welchen ANDEREN Themen es zusammenhängt (Nachbarthemen außerhalb des Projektinhalts).
|
||||||
6. Voraussetzungen: was man können oder haben muss, um es zu nutzen bzw. zu verstehen.
|
6. Voraussetzungen: welche Themen man vorher bearbeitet haben sollte, um das Projekt zu verstehen.
|
||||||
7. Moderne vs. veraltete Teile: was aktueller Stand ist — und was als Altlast gilt (falls nichts veraltet ist, ausdrücklich notieren).
|
7. Neuerungen vs. Veraltetes: was im Themenfeld in den letzten Jahren neu dazugekommen ist — und was nicht mehr verwendet wird (falls es nichts gibt, jeweils ausdrücklich notieren).
|
||||||
|
|
||||||
Schreibe NUR die Markdown-Datei nach: {out_path}
|
Schreibe NUR die Markdown-Datei nach: {out_path}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ Sammle die Faktenbasis für einen OnePager — ein Übersichtsblatt auf einer Se
|
|||||||
Recherchiere gezielt diese Dimensionen:
|
Recherchiere gezielt diese Dimensionen:
|
||||||
1. Kurzbeschreibung: Was ist "{topic}" in 1–2 Sätzen?
|
1. Kurzbeschreibung: Was ist "{topic}" in 1–2 Sätzen?
|
||||||
2. Technische Daten: Art/Typ, aktuelle Version/Stand, Lizenz/Kosten, Verbreitung.
|
2. Technische Daten: Art/Typ, aktuelle Version/Stand, Lizenz/Kosten, Verbreitung.
|
||||||
3. Kerneigenschaften: die prägenden Eigenschaften und Merkmale des Systems.
|
3. Inhaltsübersicht: Was erwartet einen im Thema — die wichtigsten Inhalte/Teilgebiete.
|
||||||
4. Beispiel: ein minimales, typisches Code-/Anwendungsbeispiel.
|
4. Beispiel: ein minimales, typisches Code-/Anwendungsbeispiel.
|
||||||
5. Zusammenhänge: mit welchen Systemen/Themen es zusammenhängt (Ökosystem, Nachbarn, typische Kombinationen).
|
5. Zusammenhänge: mit welchen ANDEREN Themen es zusammenhängt (Nachbarthemen außerhalb von "{topic}").
|
||||||
6. Voraussetzungen: was man vorher können oder haben muss.
|
6. Voraussetzungen: welche Themen man vorher bearbeitet haben sollte.
|
||||||
7. Moderne vs. veraltete Features: was heute verwendet wird — und was es noch gibt, aber nicht mehr verwendet werden sollte (falls nichts veraltet ist, ausdrücklich notieren).
|
7. Neuerungen vs. Veraltetes: was in den letzten Jahren neu dazugekommen ist — und was nicht mehr verwendet wird (falls es nichts gibt, jeweils ausdrücklich notieren).
|
||||||
|
|
||||||
Schreibe NUR die Markdown-Datei nach: {out_path}
|
Schreibe NUR die Markdown-Datei nach: {out_path}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user