import asyncio import uuid from datetime import datetime, timezone from pathlib import Path 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, 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") @router.get("/formats") async def get_formats(): return FORMAT_META @router.post("/guides", response_model=GuideResponse) async def create(req: GuideCreateRequest): now = datetime.now(timezone.utc).isoformat() guide = { "id": str(uuid.uuid4()), "topic": req.topic.strip(), "format": req.format, "instructions": req.instructions.strip(), "status": "queued", "progress": None, "html_path": None, "pdf_path": None, "created_at": now, "updated_at": now, } await create_guide(guide) asyncio.create_task(generate_guide(guide["id"], guide["topic"], guide["format"], guide["instructions"])) return guide @router.get("/guides", response_model=list[GuideResponse]) async def list_all(): return await list_guides() @router.get("/guides/{guide_id}", response_model=GuideResponse) async def get_one(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") return guide @router.get("/guides/{guide_id}/html") async def download_html(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") if guide["status"] != "done" or not guide["html_path"]: raise HTTPException(404, "HTML nicht verfügbar") path = Path(guide["html_path"]) if not path.exists(): raise HTTPException(404, "Datei nicht gefunden") return FileResponse(path, filename=f"{guide['topic']}-{guide['format']}.html", media_type="text/html") @router.post("/guides/{guide_id}/rework") async def rework(guide_id: str, req: GuideReworkRequest): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") if guide["status"] != "done": raise HTTPException(400, "Guide muss fertig sein") asyncio.create_task(rework_guide(guide_id, guide["topic"], guide["format"], req.instructions.strip())) return {"ok": True} @router.post("/guides/{guide_id}/cancel") async def cancel(guide_id: str): cancelled = await cancel_guide(guide_id) if not cancelled: raise HTTPException(404, "Kein aktiver Prozess gefunden") return {"ok": True} @router.get("/guides/{guide_id}/pdf") async def download_pdf(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") if guide["status"] != "done" or not guide["pdf_path"]: raise HTTPException(404, "PDF nicht verfügbar") path = Path(guide["pdf_path"]) if not path.exists(): raise HTTPException(404, "Datei nicht gefunden") return FileResponse(path, filename=f"{guide['topic']}-{guide['format']}.pdf", media_type="application/pdf") @router.delete("/guides/{guide_id}") async def remove(guide_id: str): guide = await get_guide(guide_id) if guide is None: raise HTTPException(404, "Guide nicht gefunden") if guide["html_path"]: Path(guide["html_path"]).unlink(missing_ok=True) if guide["pdf_path"]: 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}