Initial commit: prompt template manager
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>
This commit is contained in:
+120
@@ -0,0 +1,120 @@
|
||||
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")
|
||||
@@ -0,0 +1,13 @@
|
||||
import re
|
||||
|
||||
VAR_RE = re.compile(r'\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}')
|
||||
|
||||
|
||||
def extract_vars(template: str) -> list[str]:
|
||||
seen = set()
|
||||
result = []
|
||||
for m in VAR_RE.finditer(template):
|
||||
if m.group(1) not in seen:
|
||||
seen.add(m.group(1))
|
||||
result.append(m.group(1))
|
||||
return result
|
||||
@@ -0,0 +1,73 @@
|
||||
import json
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
SETTINGS_PATH = Path(__file__).parent.parent / "settings.json"
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def _read():
|
||||
if not SETTINGS_PATH.exists():
|
||||
return {"openai_api_key": "", "presets": []}
|
||||
return json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _write(data):
|
||||
tmp = SETTINGS_PATH.with_suffix(".tmp")
|
||||
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
tmp.replace(SETTINGS_PATH)
|
||||
|
||||
|
||||
def list_presets():
|
||||
return _read()["presets"]
|
||||
|
||||
|
||||
def create_preset(name, description, template, variables):
|
||||
data = _read()
|
||||
preset = {
|
||||
"id": secrets.token_hex(4),
|
||||
"name": name,
|
||||
"description": description,
|
||||
"template": template,
|
||||
"variables": variables,
|
||||
"created_at": _now(),
|
||||
"updated_at": _now(),
|
||||
}
|
||||
data["presets"].append(preset)
|
||||
_write(data)
|
||||
return preset
|
||||
|
||||
|
||||
def update_preset(preset_id, fields):
|
||||
data = _read()
|
||||
for p in data["presets"]:
|
||||
if p["id"] == preset_id:
|
||||
p.update(fields)
|
||||
p["updated_at"] = _now()
|
||||
_write(data)
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def delete_preset(preset_id):
|
||||
data = _read()
|
||||
before = len(data["presets"])
|
||||
data["presets"] = [p for p in data["presets"] if p["id"] != preset_id]
|
||||
if len(data["presets"]) == before:
|
||||
return False
|
||||
_write(data)
|
||||
return True
|
||||
|
||||
|
||||
def get_openai_key():
|
||||
return _read().get("openai_api_key", "")
|
||||
|
||||
|
||||
def set_openai_key(key: str):
|
||||
data = _read()
|
||||
data["openai_api_key"] = key
|
||||
_write(data)
|
||||
Reference in New Issue
Block a user