#!/usr/bin/env python3 """PromptGarage UI - FastAPI web application.""" import os from pathlib import Path from typing import Optional from fastapi import FastAPI, Form, Query, Request from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates import db_client as db app = FastAPI(title="PromptGarage", description="Web UI for your prompts") # Setup templates BASE_DIR = Path(__file__).parent templates = Jinja2Templates(directory=BASE_DIR / "templates") # Mount static files app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") # --- Routes --- @app.get("/", response_class=RedirectResponse) async def root(): return RedirectResponse("/prompts") @app.get("/prompts", name="prompts_list") async def prompts_list( request: Request, q: Optional[str] = Query(None, description="Search query"), tag: Optional[str] = Query(None, description="Filter by tag"), starred: Optional[int] = Query(None, description="Filter starred (1)"), page: int = Query(1, ge=1, description="Page number"), limit: int = Query(20, ge=1, le=100, description="Items per page"), ): offset = (page - 1) * limit # Get all tags for filter dropdown all_tags = db.get_stats()["by_tag"] # Search or list if q: prompts = db.search_fts(q) # Apply tag/star filters manually after FTS search filtered = [] for p in prompts: if tag and tag not in (p["tags"] or ""): continue if starred is not None and p["is_starred"] != starred: continue filtered.append(p) prompts = filtered[offset:offset + limit] total = len(filtered) else: # Build query params for list_prompts prompts = db.list_prompts( starred=bool(starred) if starred is not None else None, tag=tag, limit=limit + offset ) if offset > 0: prompts = prompts[offset:] total = len(db.list_prompts( starred=bool(starred) if starred is not None else None, tag=tag, limit=100000 )) total_pages = (total + limit - 1) // limit if total > 0 else 1 return templates.TemplateResponse("prompts/list.html", { "request": request, "prompts": prompts, "all_tags": all_tags, "q": q, "selected_tag": tag, "starred_filter": starred, "page": page, "limit": limit, "total": total, "total_pages": total_pages, }) @app.get("/prompts/new", name="prompts_new") async def prompts_new(request: Request): stats = db.get_stats() return templates.TemplateResponse("prompts/new.html", { "request": request, "all_tags": stats["by_tag"], }) @app.post("/prompts/new") async def prompts_create( title: str = Form(...), content: str = Form(...), tags: Optional[str] = Form(None), model: Optional[str] = Form(None), ): prompt_id = db.add_prompt(title=title, content=content, tags=tags, model=model) return RedirectResponse(f"/prompts/{prompt_id}", status_code=303) @app.get("/prompts/{prompt_id}", name="prompts_detail") async def prompts_detail(request: Request, prompt_id: int): prompt = db.get_prompt(prompt_id) if not prompt: return templates.TemplateResponse("error.html", { "request": request, "error": f"Prompt #{prompt_id} not found", }, status_code=404) return templates.TemplateResponse("prompts/detail.html", { "request": request, "prompt": prompt, }) @app.post("/prompts/{prompt_id}/star") async def prompts_toggle_star(prompt_id: int): result = db.toggle_star(prompt_id) if result is None: return {"error": "Prompt not found"} return {"is_starred": result} @app.post("/prompts/{prompt_id}/rate") async def prompts_set_rating(prompt_id: int, rating: int = Query(..., ge=1, le=5)): db.set_rating(prompt_id, rating) return {"rating": rating} @app.post("/prompts/{prompt_id}/use") async def prompts_use(prompt_id: int): db.increment_use(prompt_id) prompt = db.get_prompt(prompt_id) if prompt: return {"use_count": prompt["use_count"]} return {"error": "Prompt not found"} @app.get("/prompts/{prompt_id}/edit", name="prompts_edit") async def prompts_edit(request: Request, prompt_id: int): prompt = db.get_prompt(prompt_id) if not prompt: return templates.TemplateResponse("error.html", { "request": request, "error": f"Prompt #{prompt_id} not found", }, status_code=404) stats = db.get_stats() return templates.TemplateResponse("prompts/edit.html", { "request": request, "prompt": prompt, "all_tags": stats["by_tag"], }) @app.post("/prompts/{prompt_id}/edit") async def prompts_update( prompt_id: int, title: str = Form(...), content: str = Form(...), tags: Optional[str] = Form(None), model: Optional[str] = Form(None), ): db.update_prompt(prompt_id, title=title, content=content, tags=tags, model=model) return RedirectResponse(f"/prompts/{prompt_id}", status_code=303) @app.post("/prompts/{prompt_id}/delete") async def prompts_delete(prompt_id: int): db.delete_prompt(prompt_id) return RedirectResponse("/prompts", status_code=303) @app.get("/stats", name="stats") async def stats_page(request: Request): stats = db.get_stats() # Calculate rating distribution all_prompts = db.list_prompts(limit=100000) rating_dist = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0} for p in all_prompts: r = p["rating"] or 0 rating_dist[r] = rating_dist.get(r, 0) + 1 # Get top tags top_tags = sorted(stats["by_tag"].items(), key=lambda x: -x[1])[:15] return templates.TemplateResponse("stats.html", { "request": request, "stats": stats, "rating_dist": rating_dist, "top_tags": top_tags, }) if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)