20f06d0a57
React + Tailwind frontend, FastAPI backend, settings.json storage. Supports preset CRUD, live variable filling, and AI rewrite via OpenAI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
121 lines
4.0 KiB
Python
121 lines
4.0 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.responses import FileResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from openai import AsyncOpenAI
|
|
from pydantic import BaseModel
|
|
|
|
import backend.storage as storage
|
|
from backend.models import extract_vars
|
|
|
|
DIST = Path(__file__).parent.parent / "frontend" / "dist"
|
|
OPENAI_MODEL = os.getenv("OPENAI_MODEL", "gpt-4.1")
|
|
|
|
app = FastAPI()
|
|
|
|
|
|
# ── Models ────────────────────────────────────────────────────────────────────
|
|
|
|
class PresetBody(BaseModel):
|
|
name: str
|
|
description: str = ""
|
|
template: str
|
|
|
|
|
|
class SettingsBody(BaseModel):
|
|
openai_api_key: str
|
|
|
|
|
|
class RewriteBody(BaseModel):
|
|
prompt: str
|
|
|
|
|
|
# ── Presets ───────────────────────────────────────────────────────────────────
|
|
|
|
@app.get("/api/presets")
|
|
def get_presets():
|
|
return {"presets": storage.list_presets()}
|
|
|
|
|
|
@app.post("/api/presets", status_code=201)
|
|
def create_preset(body: PresetBody):
|
|
variables = extract_vars(body.template)
|
|
return storage.create_preset(body.name, body.description, body.template, variables)
|
|
|
|
|
|
@app.put("/api/presets/{preset_id}")
|
|
def update_preset(preset_id: str, body: PresetBody):
|
|
variables = extract_vars(body.template)
|
|
result = storage.update_preset(preset_id, {
|
|
"name": body.name,
|
|
"description": body.description,
|
|
"template": body.template,
|
|
"variables": variables,
|
|
})
|
|
if result is None:
|
|
raise HTTPException(status_code=404, detail="Preset not found")
|
|
return result
|
|
|
|
|
|
@app.delete("/api/presets/{preset_id}")
|
|
def delete_preset(preset_id: str):
|
|
if not storage.delete_preset(preset_id):
|
|
raise HTTPException(status_code=404, detail="Preset not found")
|
|
return {"ok": True}
|
|
|
|
|
|
# ── Settings ──────────────────────────────────────────────────────────────────
|
|
|
|
@app.get("/api/settings")
|
|
def get_settings():
|
|
key = storage.get_openai_key()
|
|
return {"openai_api_key_set": bool(key)}
|
|
|
|
|
|
@app.post("/api/settings")
|
|
def update_settings(body: SettingsBody):
|
|
storage.set_openai_key(body.openai_api_key)
|
|
return {"ok": True}
|
|
|
|
|
|
# ── AI Rewrite ────────────────────────────────────────────────────────────────
|
|
|
|
@app.post("/api/rewrite")
|
|
async def rewrite_prompt(body: RewriteBody):
|
|
key = storage.get_openai_key()
|
|
if not key:
|
|
raise HTTPException(status_code=400, detail="OpenAI API key not configured")
|
|
|
|
client = AsyncOpenAI(api_key=key)
|
|
try:
|
|
response = await client.chat.completions.create(
|
|
model=OPENAI_MODEL,
|
|
messages=[
|
|
{
|
|
"role": "system",
|
|
"content": (
|
|
"You are a prompt engineer. Rewrite the user's prompt to be clearer, "
|
|
"more specific, and more effective for an AI assistant. "
|
|
"Return only the rewritten prompt, no explanation."
|
|
),
|
|
},
|
|
{"role": "user", "content": body.prompt},
|
|
],
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=502, detail=str(e))
|
|
|
|
return {"rewritten": response.choices[0].message.content}
|
|
|
|
|
|
# ── SPA static serving (must be last) ────────────────────────────────────────
|
|
|
|
if DIST.exists():
|
|
app.mount("/assets", StaticFiles(directory=DIST / "assets"), name="assets")
|
|
|
|
@app.get("/{full_path:path}")
|
|
def serve_spa(full_path: str):
|
|
return FileResponse(DIST / "index.html")
|