Really-amin commited on
Commit
d70685c
·
verified ·
1 Parent(s): 755e64b

Upload PromptForge v1.0 — Structured prompt generator for Google AI Studio

Browse files
.env.example ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PromptForge — Environment Variables
2
+ # Copy this file to .env and fill in your values.
3
+ # NEVER commit .env to version control.
4
+
5
+ # ── Google AI Studio / Gemini ──────────────────────────────────────
6
+ # Get your key at: https://aistudio.google.com/app/apikey
7
+ GOOGLE_API_KEY=your_google_api_key_here
8
+
9
+ # ── Hugging Face Inference API ─────────────────────────────────────
10
+ # Get your key at: https://huggingface.co/settings/tokens
11
+ HF_API_KEY=your_huggingface_api_key_here
12
+
13
+ # ── Server settings ────────────────────────────────────────────────
14
+ HOST=0.0.0.0
15
+ PORT=8000
16
+ LOG_DIR=./logs
.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .env
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ .pytest_cache/
6
+ logs/
7
+ *.egg-info/
8
+ dist/
9
+ .venv/
10
+ venv/
11
+ node_modules/
12
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ LABEL maintainer="PromptForge"
4
+ LABEL description="Structured prompt generator for Google AI Studio"
5
+
6
+ # System deps
7
+ RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
8
+
9
+ WORKDIR /app
10
+
11
+ # Install Python deps
12
+ COPY backend/requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application
16
+ COPY backend/ ./backend/
17
+ COPY frontend/ ./frontend/
18
+
19
+ # Logs directory
20
+ RUN mkdir -p logs
21
+
22
+ WORKDIR /app/backend
23
+
24
+ # Environment defaults (override in your deployment / .env)
25
+ ENV HOST=0.0.0.0
26
+ ENV PORT=8000
27
+ ENV LOG_DIR=/app/logs
28
+ ENV PYTHONUNBUFFERED=1
29
+
30
+ EXPOSE 8000
31
+
32
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
33
+ CMD curl -f http://localhost:8000/health || exit 1
34
+
35
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Promptforge
3
- emoji: 🏃
4
- colorFrom: gray
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
+ # ⚙️ PromptForge
2
+
3
+ **PromptForge** is a cloud-ready FastAPI service that converts raw user instructions into
4
+ **structured, ready-to-use prompts** for Google AI Studio. It follows a 5-step guided workflow
5
+ with a clean HTML/JS frontend, optional AI enhancement via Google Gemini or Hugging Face, and
6
+ persistent prompt history.
7
+
8
+ ---
9
+
10
+ ## 📁 Folder Structure
11
+
12
+ ```
13
+ promptforge/
14
+ ├── backend/
15
+ │ ├── main.py ← FastAPI server + all routes
16
+ │ ├── schemas.py ← Pydantic request/response models
17
+ │ ├── prompt_logic.py ← Core instruction → structured prompt engine
18
+ │ ├── ai_client.py ← Hugging Face & Google Gemini integration
19
+ │ ├── store.py ← In-memory store with JSON persistence
20
+ │ ├── requirements.txt
21
+ │ └── tests/
22
+ │ └── test_promptforge.py
23
+ ├── frontend/
24
+ │ ├── index.html ← Single-page UI
25
+ │ ├── client.js ← Fetch-based API client
26
+ │ └── style.css ← Dark-mode responsive styling
27
+ ├── logs/ ← Auto-created; stores prompt_history.json
28
+ ├── Dockerfile
29
+ ├── docker-compose.yml
30
+ ├── .env.example ← Copy to .env and fill API keys
31
+ ├── .gitignore
32
+ └── README.md
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🔄 5-Step Workflow
38
+
39
+ | Step | Endpoint | Description |
40
+ |------|----------|-------------|
41
+ | **0** | `POST /api/generate` | Accept raw instruction → produce JSON manifest |
42
+ | **1** | *(frontend)* | Display manifest for human review |
43
+ | **2** | `POST /api/approve` | Approve (with optional edits) → finalize prompt |
44
+ | **3** | *(frontend)* | Preview plain-text + JSON output |
45
+ | **4** | `POST /api/export` | Download as `.json` or `.txt` |
46
+ | **5** | `POST /api/refine` | Iterate with feedback → new manifest version |
47
+
48
+ ---
49
+
50
+ ## 🚀 Running Locally
51
+
52
+ ### 1. Clone & set up environment
53
+
54
+ ```bash
55
+ git clone <your-repo-url>
56
+ cd promptforge
57
+
58
+ # Create virtual environment
59
+ python -m venv .venv
60
+ source .venv/bin/activate # Windows: .venv\Scripts\activate
61
+
62
+ # Install dependencies
63
+ pip install -r backend/requirements.txt
64
+ ```
65
+
66
+ ### 2. Configure API keys
67
+
68
+ ```bash
69
+ cp .env.example .env
70
+ # Open .env and fill in your keys:
71
+ # GOOGLE_API_KEY=...
72
+ # HF_API_KEY=...
73
+ ```
74
+
75
+ > **Security note:** API keys entered in the frontend are sent only over HTTPS
76
+ > and are never logged or stored. Keys in `.env` are for server-side defaults;
77
+ > frontend keys override them per-request.
78
+
79
+ ### 3. Start the server
80
+
81
+ ```bash
82
+ cd backend
83
+ uvicorn main:app --reload --host 0.0.0.0 --port 8000
84
+ ```
85
+
86
+ Open **http://localhost:8000** in your browser.
87
+ API docs: **http://localhost:8000/docs**
88
+
89
+ ---
90
+
91
+ ## 🐳 Docker Deployment
92
+
93
+ ```bash
94
+ # Build and run
95
+ docker-compose up --build
96
+
97
+ # Or production detached
98
+ docker-compose up -d
99
+ ```
100
+
101
+ ---
102
+
103
+ ## ☁️ Cloud Deployment (Google Cloud Run)
104
+
105
+ ```bash
106
+ # Build container
107
+ gcloud builds submit --tag gcr.io/YOUR_PROJECT/promptforge
108
+
109
+ # Deploy
110
+ gcloud run deploy promptforge \
111
+ --image gcr.io/YOUR_PROJECT/promptforge \
112
+ --platform managed \
113
+ --region us-central1 \
114
+ --allow-unauthenticated \
115
+ --set-env-vars "LOG_DIR=/tmp/logs"
116
+ ```
117
+
118
+ For Cloud Run, set secrets via Secret Manager:
119
+
120
+ ```bash
121
+ gcloud secrets create GOOGLE_API_KEY --data-file=- <<< "your-key"
122
+ gcloud secrets create HF_API_KEY --data-file=- <<< "your-key"
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 🧪 Running Tests
128
+
129
+ ```bash
130
+ cd backend
131
+ pytest tests/ -v
132
+ ```
133
+
134
+ Expected output: **19 tests passing**.
135
+
136
  ---
137
+
138
+ ## 📡 API Reference
139
+
140
+ ### `POST /api/generate`
141
+
142
+ ```json
143
+ {
144
+ "instruction": "Generate a TypeScript React component with TailwindCSS and unit tests.",
145
+ "output_format": "both",
146
+ "provider": "google",
147
+ "api_key": "YOUR_KEY_HERE",
148
+ "enhance": true,
149
+ "extra_context": "Must support dark mode and be accessible."
150
+ }
151
+ ```
152
+
153
+ **Response:** `GenerateResponse` with full `PromptManifest`.
154
+
155
+ ---
156
+
157
+ ### `POST /api/approve`
158
+
159
+ ```json
160
+ {
161
+ "prompt_id": "<uuid>",
162
+ "edits": {
163
+ "role": "Principal Software Engineer",
164
+ "constraints": ["Use TypeScript strict mode.", "Tests must use Vitest."]
165
+ }
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ### `POST /api/export`
172
+
173
+ ```json
174
+ { "prompt_id": "<uuid>", "export_format": "json" }
175
+ ```
176
+
177
+ ---
178
+
179
+ ### `POST /api/refine`
180
+
181
+ ```json
182
+ {
183
+ "prompt_id": "<uuid>",
184
+ "feedback": "Add ARIA labels and keyboard navigation support.",
185
+ "provider": "none"
186
+ }
187
+ ```
188
+
189
+ ---
190
+
191
+ ### `GET /api/history`
192
+
193
+ Returns all previously generated prompts with status and version info.
194
+
195
+ ---
196
+
197
+ ## 🏗️ Example: Full Prompt Output
198
+
199
+ **Instruction:** `"Generate a TypeScript React component with TailwindCSS and unit tests."`
200
+
201
+ ```
202
+ ## ROLE
203
+ You are a Senior Frontend Engineer.
204
+
205
+ ## TASK
206
+ Generate a TypeScript React component with TailwindCSS and unit tests.
207
+
208
+ ## INPUT FORMAT
209
+ A plain-text string describing the user's request or the content to process.
210
+
211
+ ## OUTPUT FORMAT
212
+ Source code inside a properly labeled fenced code block. Include a brief
213
+ explanation before and after the block.
214
+
215
+ ## CONSTRAINTS
216
+ 1. Use TypeScript with strict mode enabled.
217
+ 2. Use TailwindCSS utility classes exclusively; avoid custom CSS unless unavoidable.
218
+ 3. Include comprehensive unit tests with >80% coverage.
219
+
220
+ ## STYLE & TONE
221
+ Professional and clear; balance technical accuracy with readability.
222
+
223
+ ## SAFETY GUIDELINES
224
+ 1. Do not produce harmful, misleading, or unethical content.
225
+ 2. Respect intellectual property; do not reproduce copyrighted material verbatim.
226
+ 3. If the request is ambiguous or potentially harmful, ask for clarification.
227
+ 4. Adhere to Google AI Studio usage policies and Responsible AI guidelines.
228
+
229
+ ## FEW-SHOT EXAMPLES
230
+ **Input:** Create a Button component.
231
+ **Output:** ```tsx
232
+ interface ButtonProps { label: string; onClick: () => void; }
233
+ export const Button = ({ label, onClick }: ButtonProps) => (
234
+ <button onClick={onClick} className='px-4 py-2 bg-blue-600 text-white rounded'>{label}</button>
235
+ );
236
+ ```
237
+
238
+ ---
239
+ *Prompt generated by PromptForge — compatible with Google AI Studio.*
240
+ ```
241
+
242
  ---
243
 
244
+ ## 🔐 Security Notes
245
+
246
+ - API keys are accepted per-request and **never persisted** in logs or storage.
247
+ - The `store.py` logs contain instruction text and prompt content but strip API keys.
248
+ - For production: add authentication (e.g., FastAPI `Depends` with Bearer tokens), enforce HTTPS, and set `allow_origins` in CORS middleware to your domain only.
249
+
250
+ ---
251
+
252
+ ## 📝 License
253
+
254
+ MIT — use freely, modify as needed.
backend/ai_client.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PromptForge — AI client interface.
3
+ Supports optional enhancement via:
4
+ - Hugging Face Inference API (text-generation models)
5
+ - Google AI Studio / Gemini API (generateContent)
6
+ """
7
+ from __future__ import annotations
8
+ import logging
9
+ from typing import Optional
10
+
11
+ import httpx
12
+
13
+ logger = logging.getLogger("promptforge.ai_client")
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Hugging Face
17
+ # ---------------------------------------------------------------------------
18
+
19
+ HF_API_BASE = "https://api-inference.huggingface.co/models"
20
+ HF_DEFAULT_MODEL = "mistralai/Mistral-7B-Instruct-v0.2"
21
+
22
+
23
+ async def enhance_with_huggingface(
24
+ raw_prompt: str,
25
+ api_key: str,
26
+ model: str = HF_DEFAULT_MODEL,
27
+ max_new_tokens: int = 512,
28
+ ) -> str:
29
+ """
30
+ Call the Hugging Face Inference API to enhance a raw prompt text.
31
+ Returns the enhanced prompt string, or the original on failure.
32
+ """
33
+ system_instruction = (
34
+ "You are an expert prompt engineer specializing in Google AI Studio prompts. "
35
+ "Given a draft prompt, improve its clarity, specificity, and structure. "
36
+ "Return ONLY the improved prompt text — no commentary."
37
+ )
38
+ payload = {
39
+ "inputs": f"<s>[INST] {system_instruction}\n\nDRAFT PROMPT:\n{raw_prompt}\n\nIMPROVED PROMPT: [/INST]",
40
+ "parameters": {"max_new_tokens": max_new_tokens, "return_full_text": False},
41
+ }
42
+ headers = {"Authorization": f"Bearer {api_key}"}
43
+ url = f"{HF_API_BASE}/{model}"
44
+
45
+ try:
46
+ async with httpx.AsyncClient(timeout=30.0) as client:
47
+ resp = await client.post(url, json=payload, headers=headers)
48
+ resp.raise_for_status()
49
+ data = resp.json()
50
+ # HF returns a list of dicts with "generated_text"
51
+ if isinstance(data, list) and data:
52
+ return data[0].get("generated_text", raw_prompt).strip()
53
+ return raw_prompt
54
+ except Exception as exc:
55
+ logger.warning("HuggingFace enhancement failed: %s", exc)
56
+ return raw_prompt
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Google AI Studio / Gemini
61
+ # ---------------------------------------------------------------------------
62
+
63
+ GOOGLE_AI_BASE = "https://generativelanguage.googleapis.com/v1beta"
64
+ GOOGLE_DEFAULT_MODEL = "gemini-1.5-flash"
65
+
66
+
67
+ async def enhance_with_google(
68
+ raw_prompt: str,
69
+ api_key: str,
70
+ model: str = GOOGLE_DEFAULT_MODEL,
71
+ ) -> str:
72
+ """
73
+ Call the Google Generative Language API (Gemini) to enhance a prompt.
74
+ Returns the enhanced prompt string, or the original on failure.
75
+ """
76
+ url = f"{GOOGLE_AI_BASE}/models/{model}:generateContent?key={api_key}"
77
+ system_part = (
78
+ "You are an expert prompt engineer. "
79
+ "Improve the following draft prompt for use in Google AI Studio. "
80
+ "Return ONLY the improved prompt — no preamble or commentary."
81
+ )
82
+ payload = {
83
+ "contents": [
84
+ {
85
+ "parts": [
86
+ {"text": f"{system_part}\n\nDRAFT PROMPT:\n{raw_prompt}"}
87
+ ]
88
+ }
89
+ ],
90
+ "generationConfig": {"maxOutputTokens": 1024, "temperature": 0.4},
91
+ }
92
+
93
+ try:
94
+ async with httpx.AsyncClient(timeout=30.0) as client:
95
+ resp = await client.post(url, json=payload)
96
+ resp.raise_for_status()
97
+ data = resp.json()
98
+ candidates = data.get("candidates", [])
99
+ if candidates:
100
+ parts = candidates[0].get("content", {}).get("parts", [])
101
+ if parts:
102
+ return parts[0].get("text", raw_prompt).strip()
103
+ return raw_prompt
104
+ except Exception as exc:
105
+ logger.warning("Google AI enhancement failed: %s", exc)
106
+ return raw_prompt
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Dispatcher
111
+ # ---------------------------------------------------------------------------
112
+
113
+ async def enhance_prompt(
114
+ raw_prompt: str,
115
+ provider: str,
116
+ api_key: Optional[str],
117
+ ) -> tuple[str, str]:
118
+ """
119
+ Dispatch enhancement to the correct provider.
120
+ Returns (enhanced_text, notes_string).
121
+ """
122
+ if not api_key:
123
+ return raw_prompt, "No API key provided — skipping AI enhancement."
124
+
125
+ if provider == "huggingface":
126
+ enhanced = await enhance_with_huggingface(raw_prompt, api_key)
127
+ notes = f"Enhanced via Hugging Face ({HF_DEFAULT_MODEL})."
128
+ elif provider == "google":
129
+ enhanced = await enhance_with_google(raw_prompt, api_key)
130
+ notes = f"Enhanced via Google Gemini ({GOOGLE_DEFAULT_MODEL})."
131
+ else:
132
+ return raw_prompt, "Provider 'none' selected — no enhancement applied."
133
+
134
+ if enhanced == raw_prompt:
135
+ notes += " (Enhancement returned unchanged text — possible API issue.)"
136
+
137
+ return enhanced, notes
backend/main.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PromptForge — FastAPI server
3
+ Run: uvicorn main:app --reload --host 0.0.0.0 --port 8000
4
+ """
5
+ from __future__ import annotations
6
+ import logging
7
+ import os
8
+ from pathlib import Path
9
+
10
+ from fastapi import FastAPI, HTTPException, Request, status
11
+ from fastapi.middleware.cors import CORSMiddleware
12
+ from fastapi.responses import HTMLResponse, JSONResponse
13
+ from fastapi.staticfiles import StaticFiles
14
+
15
+ import store
16
+ from ai_client import enhance_prompt
17
+ from prompt_logic import apply_edits, build_manifest, refine_with_feedback
18
+ from schemas import (
19
+ ApproveRequest,
20
+ ApproveResponse,
21
+ ExportRequest,
22
+ ExportResponse,
23
+ GenerateRequest,
24
+ GenerateResponse,
25
+ HistoryResponse,
26
+ RefineRequest,
27
+ )
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Logging
31
+ # ---------------------------------------------------------------------------
32
+ logging.basicConfig(
33
+ level=logging.INFO,
34
+ format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
35
+ )
36
+ logger = logging.getLogger("promptforge.main")
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # App bootstrap
40
+ # ---------------------------------------------------------------------------
41
+ app = FastAPI(
42
+ title="PromptForge",
43
+ description="Autonomously generate structured, ready-to-use prompts for Google AI Studio.",
44
+ version="1.0.0",
45
+ docs_url="/docs",
46
+ redoc_url="/redoc",
47
+ )
48
+
49
+ app.add_middleware(
50
+ CORSMiddleware,
51
+ allow_origins=["*"], # Lock this down in production
52
+ allow_methods=["*"],
53
+ allow_headers=["*"],
54
+ )
55
+
56
+ # Serve frontend from /frontend directory
57
+ _FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
58
+ if _FRONTEND_DIR.exists():
59
+ app.mount("/static", StaticFiles(directory=str(_FRONTEND_DIR)), name="static")
60
+
61
+
62
+ @app.on_event("startup")
63
+ async def _startup() -> None:
64
+ store.load_from_disk()
65
+ logger.info("PromptForge started. Visit http://localhost:8000")
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Root — serve the HTML frontend
70
+ # ---------------------------------------------------------------------------
71
+
72
+ @app.get("/", response_class=HTMLResponse, tags=["Frontend"])
73
+ async def serve_frontend() -> HTMLResponse:
74
+ index = _FRONTEND_DIR / "index.html"
75
+ if index.exists():
76
+ return HTMLResponse(content=index.read_text(), status_code=200)
77
+ return HTMLResponse(
78
+ content="<h1>PromptForge API is running.</h1><p>Visit <a href='/docs'>/docs</a> for API reference.</p>",
79
+ status_code=200,
80
+ )
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Step 0 + 1: Accept instruction → return manifest
85
+ # ---------------------------------------------------------------------------
86
+
87
+ @app.post("/api/generate", response_model=GenerateResponse, tags=["Prompts"])
88
+ async def generate_prompt(req: GenerateRequest) -> GenerateResponse:
89
+ """
90
+ **Step 1** — Accept a raw user instruction and return a JSON manifest
91
+ of the proposed structured prompt. The manifest must be approved before
92
+ the prompt is finalized.
93
+ """
94
+ logger.info("GENERATE | instruction=%r | enhance=%s | provider=%s",
95
+ req.instruction[:80], req.enhance, req.provider)
96
+
97
+ manifest = build_manifest(
98
+ instruction=req.instruction,
99
+ extra_context=req.extra_context,
100
+ )
101
+
102
+ # Optional AI enhancement pass
103
+ if req.enhance and req.provider != "none":
104
+ enhanced_text, notes = await enhance_prompt(
105
+ raw_prompt=manifest.structured_prompt.raw_prompt_text,
106
+ provider=req.provider,
107
+ api_key=req.api_key,
108
+ )
109
+ # Patch the raw_prompt_text with the enhanced version
110
+ sp = manifest.structured_prompt.model_copy(
111
+ update={"raw_prompt_text": enhanced_text}
112
+ )
113
+ manifest = manifest.model_copy(
114
+ update={"structured_prompt": sp, "enhancement_notes": notes}
115
+ )
116
+ logger.info("ENHANCE | %s", notes)
117
+
118
+ store.save(manifest)
119
+ logger.info("SAVED | prompt_id=%s version=%d", manifest.prompt_id, manifest.version)
120
+
121
+ return GenerateResponse(
122
+ success=True,
123
+ prompt_id=manifest.prompt_id,
124
+ manifest=manifest,
125
+ )
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Step 2 + 3: Human approval
130
+ # ---------------------------------------------------------------------------
131
+
132
+ @app.post("/api/approve", response_model=ApproveResponse, tags=["Prompts"])
133
+ async def approve_prompt(req: ApproveRequest) -> ApproveResponse:
134
+ """
135
+ **Step 2/3** — Approve (and optionally edit) a pending manifest.
136
+ Returns the finalized structured prompt ready for Google AI Studio.
137
+ """
138
+ manifest = store.get(req.prompt_id)
139
+ if not manifest:
140
+ raise HTTPException(status_code=404, detail=f"Prompt '{req.prompt_id}' not found.")
141
+
142
+ if req.edits:
143
+ manifest = apply_edits(manifest, req.edits)
144
+ else:
145
+ manifest = manifest.model_copy(update={"status": "approved"})
146
+
147
+ store.save(manifest)
148
+ logger.info("APPROVED | prompt_id=%s", manifest.prompt_id)
149
+
150
+ return ApproveResponse(
151
+ success=True,
152
+ prompt_id=manifest.prompt_id,
153
+ message="Prompt approved and finalized.",
154
+ finalized_prompt=manifest.structured_prompt,
155
+ )
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Step 4: Export
160
+ # ---------------------------------------------------------------------------
161
+
162
+ @app.post("/api/export", response_model=ExportResponse, tags=["Prompts"])
163
+ async def export_prompt(req: ExportRequest) -> ExportResponse:
164
+ """
165
+ **Step 4** — Export a finalized prompt as JSON or plain text.
166
+ """
167
+ manifest = store.get(req.prompt_id)
168
+ if not manifest:
169
+ raise HTTPException(status_code=404, detail=f"Prompt '{req.prompt_id}' not found.")
170
+ if manifest.status not in ("approved", "exported"):
171
+ raise HTTPException(
172
+ status_code=400,
173
+ detail="Prompt must be approved before exporting. Call /api/approve first.",
174
+ )
175
+
176
+ manifest = manifest.model_copy(update={"status": "exported"})
177
+ store.save(manifest)
178
+ logger.info("EXPORTED | prompt_id=%s format=%s", manifest.prompt_id, req.export_format)
179
+
180
+ if req.export_format == "text":
181
+ data = manifest.structured_prompt.raw_prompt_text
182
+ elif req.export_format == "json":
183
+ data = manifest.structured_prompt.model_dump()
184
+ else: # both
185
+ data = {
186
+ "json": manifest.structured_prompt.model_dump(),
187
+ "text": manifest.structured_prompt.raw_prompt_text,
188
+ }
189
+
190
+ return ExportResponse(success=True, prompt_id=manifest.prompt_id, data=data)
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Step 5: Iterative refinement
195
+ # ---------------------------------------------------------------------------
196
+
197
+ @app.post("/api/refine", response_model=GenerateResponse, tags=["Prompts"])
198
+ async def refine_prompt(req: RefineRequest) -> GenerateResponse:
199
+ """
200
+ **Step 5** — Refine an existing prompt with user feedback.
201
+ Creates a new version (v+1) of the manifest.
202
+ """
203
+ manifest = store.get(req.prompt_id)
204
+ if not manifest:
205
+ raise HTTPException(status_code=404, detail=f"Prompt '{req.prompt_id}' not found.")
206
+
207
+ refined = refine_with_feedback(manifest, req.feedback)
208
+
209
+ if req.provider != "none" and req.api_key:
210
+ enhanced_text, notes = await enhance_prompt(
211
+ raw_prompt=refined.structured_prompt.raw_prompt_text,
212
+ provider=req.provider,
213
+ api_key=req.api_key,
214
+ )
215
+ sp = refined.structured_prompt.model_copy(update={"raw_prompt_text": enhanced_text})
216
+ refined = refined.model_copy(update={"structured_prompt": sp, "enhancement_notes": notes})
217
+
218
+ store.save(refined)
219
+ logger.info("REFINED | prompt_id=%s new_version=%d", refined.prompt_id, refined.version)
220
+
221
+ return GenerateResponse(
222
+ success=True,
223
+ prompt_id=refined.prompt_id,
224
+ manifest=refined,
225
+ message=f"Refined to v{refined.version} — awaiting approval.",
226
+ )
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # History
231
+ # ---------------------------------------------------------------------------
232
+
233
+ @app.get("/api/history", response_model=HistoryResponse, tags=["History"])
234
+ async def get_history() -> HistoryResponse:
235
+ """Return a list of all previously generated prompts."""
236
+ entries = store.all_entries()
237
+ return HistoryResponse(total=len(entries), entries=entries)
238
+
239
+
240
+ @app.get("/api/history/{prompt_id}", tags=["History"])
241
+ async def get_prompt(prompt_id: str) -> dict:
242
+ """Return the full manifest for a single prompt by ID."""
243
+ manifest = store.get(prompt_id)
244
+ if not manifest:
245
+ raise HTTPException(status_code=404, detail=f"Prompt '{prompt_id}' not found.")
246
+ return manifest.model_dump(mode="json")
247
+
248
+
249
+ @app.delete("/api/history/{prompt_id}", tags=["History"])
250
+ async def delete_prompt(prompt_id: str) -> JSONResponse:
251
+ """Delete a prompt from history."""
252
+ deleted = store.delete(prompt_id)
253
+ if not deleted:
254
+ raise HTTPException(status_code=404, detail=f"Prompt '{prompt_id}' not found.")
255
+ return JSONResponse({"success": True, "message": f"Prompt {prompt_id} deleted."})
256
+
257
+
258
+ # ---------------------------------------------------------------------------
259
+ # Health
260
+ # ---------------------------------------------------------------------------
261
+
262
+ @app.get("/health", tags=["System"])
263
+ async def health() -> dict:
264
+ return {"status": "ok", "service": "PromptForge", "version": "1.0.0"}
backend/prompt_logic.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PromptForge — Core logic to transform raw user instructions into structured prompts
3
+ compatible with Google AI Studio.
4
+ """
5
+ from __future__ import annotations
6
+ import re
7
+ import uuid
8
+ import textwrap
9
+ from datetime import datetime
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from schemas import PromptManifest, StructuredPrompt
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Heuristic keyword maps used for lightweight classification
17
+ # ---------------------------------------------------------------------------
18
+
19
+ _ROLE_MAP: List[tuple[List[str], str]] = [
20
+ (["react", "vue", "angular", "component", "frontend", "ui", "tailwind"], "Senior Frontend Engineer"),
21
+ (["api", "rest", "fastapi", "flask", "django", "backend", "endpoint"], "Senior Backend Engineer"),
22
+ (["sql", "database", "postgres", "mongo", "query", "schema"], "Database Architect"),
23
+ (["test", "unittest", "pytest", "jest", "coverage", "tdd"], "QA / Test Automation Engineer"),
24
+ (["devops", "docker", "kubernetes", "ci/cd", "deploy", "cloud", "terraform"], "DevOps / Cloud Engineer"),
25
+ (["machine learning", "ml", "model", "training", "dataset", "neural", "pytorch", "tensorflow"], "Machine Learning Engineer"),
26
+ (["data analysis", "pandas", "numpy", "visualization", "chart", "plot"], "Data Scientist"),
27
+ (["security", "auth", "oauth", "jwt", "encrypt", "pentest"], "Security Engineer"),
28
+ (["write", "blog", "article", "essay", "copy", "content"], "Professional Content Writer"),
29
+ (["summarize", "summary", "tldr", "abstract"], "Technical Summarizer"),
30
+ (["translate", "localize", "language"], "Multilingual Translation Specialist"),
31
+ ]
32
+
33
+ _STYLE_MAP: List[tuple[List[str], str]] = [
34
+ (["simple", "beginner", "basic", "easy", "novice"], "Clear, simple language; avoid jargon; explain every step."),
35
+ (["expert", "advanced", "senior", "professional"], "Concise, technically precise; assume expert knowledge."),
36
+ (["formal", "report", "documentation", "spec"], "Formal prose; structured headings; professional tone."),
37
+ (["creative", "story", "narrative", "fiction"], "Engaging, vivid language; narrative structure."),
38
+ ]
39
+
40
+ _SAFETY_DEFAULTS: List[str] = [
41
+ "Do not produce harmful, misleading, or unethical content.",
42
+ "Respect intellectual property; do not reproduce copyrighted material verbatim.",
43
+ "If the request is ambiguous or potentially harmful, ask for clarification rather than guessing.",
44
+ "Adhere to Google AI Studio usage policies and Responsible AI guidelines.",
45
+ ]
46
+
47
+ _CONSTRAINT_PATTERNS: List[tuple[str, str]] = [
48
+ (r"\btypescript\b", "Use TypeScript with strict mode enabled."),
49
+ (r"\bpython\b", "Use Python 3.10+; follow PEP-8 style guide."),
50
+ (r"\btailwind\b", "Use TailwindCSS utility classes exclusively; avoid custom CSS unless unavoidable."),
51
+ (r"\bunit test[s]?\b|\bjest\b|\bpytest\b", "Include comprehensive unit tests with >80% coverage."),
52
+ (r"\bjson\b", "All structured data must be valid, parseable JSON."),
53
+ (r"\baccessib\w+\b", "Ensure WCAG 2.1 AA accessibility compliance."),
54
+ (r"\bresponsive\b", "Design must be fully responsive for mobile, tablet, and desktop."),
55
+ (r"\bdocker\b", "Provide a Dockerfile and docker-compose.yml."),
56
+ (r"\bno comment[s]?\b", "Do not include inline code comments."),
57
+ (r"\bcomment[s]?\b", "Include clear, concise inline comments explaining non-obvious logic."),
58
+ ]
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Public API
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def build_manifest(
66
+ instruction: str,
67
+ extra_context: Optional[str] = None,
68
+ version: int = 1,
69
+ existing_id: Optional[str] = None,
70
+ ) -> PromptManifest:
71
+ """Transform a raw instruction string into a full PromptManifest."""
72
+ prompt_id = existing_id or str(uuid.uuid4())
73
+ lower = instruction.lower()
74
+
75
+ role = _infer_role(lower)
76
+ task = _infer_task(instruction)
77
+ input_fmt = _infer_input_format(lower)
78
+ output_fmt = _infer_output_format(lower)
79
+ constraints = _infer_constraints(lower)
80
+ style = _infer_style(lower)
81
+ safety = list(_SAFETY_DEFAULTS)
82
+ examples = _build_examples(lower, role)
83
+
84
+ raw_text = _render_raw_prompt(
85
+ role=role,
86
+ task=task,
87
+ input_fmt=input_fmt,
88
+ output_fmt=output_fmt,
89
+ constraints=constraints,
90
+ style=style,
91
+ safety=safety,
92
+ examples=examples,
93
+ extra_context=extra_context,
94
+ )
95
+
96
+ structured = StructuredPrompt(
97
+ role=role,
98
+ task=task,
99
+ input_format=input_fmt,
100
+ output_format=output_fmt,
101
+ constraints=constraints,
102
+ style=style,
103
+ safety=safety,
104
+ examples=examples,
105
+ raw_prompt_text=raw_text,
106
+ )
107
+
108
+ return PromptManifest(
109
+ prompt_id=prompt_id,
110
+ version=version,
111
+ created_at=datetime.utcnow(),
112
+ instruction=instruction,
113
+ status="pending",
114
+ structured_prompt=structured,
115
+ )
116
+
117
+
118
+ def apply_edits(manifest: PromptManifest, edits: Dict[str, Any]) -> PromptManifest:
119
+ """Apply user edits to an existing manifest and regenerate raw text."""
120
+ sp = manifest.structured_prompt.model_copy(update=edits)
121
+ sp = sp.model_copy(
122
+ update={
123
+ "raw_prompt_text": _render_raw_prompt(
124
+ role=sp.role,
125
+ task=sp.task,
126
+ input_fmt=sp.input_format,
127
+ output_fmt=sp.output_format,
128
+ constraints=sp.constraints,
129
+ style=sp.style,
130
+ safety=sp.safety,
131
+ examples=sp.examples,
132
+ )
133
+ }
134
+ )
135
+ return manifest.model_copy(
136
+ update={"structured_prompt": sp, "status": "approved"}
137
+ )
138
+
139
+
140
+ def refine_with_feedback(manifest: PromptManifest, feedback: str) -> PromptManifest:
141
+ """Naively incorporate textual feedback into the task description (pre-AI fallback)."""
142
+ new_task = manifest.structured_prompt.task + f"\n\nAdditional requirements from user feedback:\n{feedback}"
143
+ return build_manifest(
144
+ instruction=manifest.instruction + " " + feedback,
145
+ version=manifest.version + 1,
146
+ existing_id=manifest.prompt_id,
147
+ )
148
+
149
+
150
+ # ---------------------------------------------------------------------------
151
+ # Private helpers
152
+ # ---------------------------------------------------------------------------
153
+
154
+ def _infer_role(lower: str) -> str:
155
+ for keywords, role in _ROLE_MAP:
156
+ if any(kw in lower for kw in keywords):
157
+ return role
158
+ return "General AI Assistant"
159
+
160
+
161
+ def _infer_task(instruction: str) -> str:
162
+ # Sentence-case the instruction and ensure it ends with a period
163
+ task = instruction.strip()
164
+ if not task.endswith((".", "!", "?")):
165
+ task += "."
166
+ return task
167
+
168
+
169
+ def _infer_input_format(lower: str) -> str:
170
+ if any(k in lower for k in ["json", "object", "dict"]):
171
+ return "A JSON object containing the relevant fields described in the task."
172
+ if any(k in lower for k in ["file", "upload", "csv", "pdf"]):
173
+ return "A file upload (the file path or base64-encoded content will be provided)."
174
+ if any(k in lower for k in ["image", "photo", "screenshot", "diagram"]):
175
+ return "An image or screenshot provided as a URL or base64 string."
176
+ return "A plain-text string describing the user's request or the content to process."
177
+
178
+
179
+ def _infer_output_format(lower: str) -> str:
180
+ if any(k in lower for k in ["json", "structured", "object"]):
181
+ return "A well-formatted JSON object with clearly named keys. Include no extra prose outside the JSON block."
182
+ if any(k in lower for k in ["markdown", "md", "readme", "documentation"]):
183
+ return "A Markdown-formatted document with appropriate headers, code blocks, and lists."
184
+ if any(k in lower for k in ["code", "script", "function", "class", "component"]):
185
+ return "Source code inside a properly labeled fenced code block (e.g., ```typescript ... ```). Include a brief explanation before and after the block."
186
+ if any(k in lower for k in ["list", "bullet", "steps"]):
187
+ return "A numbered or bulleted list with concise, actionable items."
188
+ return "A clear, well-structured plain-text response."
189
+
190
+
191
+ def _infer_constraints(lower: str) -> List[str]:
192
+ found: List[str] = []
193
+ for pattern, constraint in _CONSTRAINT_PATTERNS:
194
+ if re.search(pattern, lower):
195
+ found.append(constraint)
196
+ if not found:
197
+ found.append("Keep the response concise and directly relevant to the task.")
198
+ return found
199
+
200
+
201
+ def _infer_style(lower: str) -> str:
202
+ for keywords, style in _STYLE_MAP:
203
+ if any(kw in lower for kw in keywords):
204
+ return style
205
+ return "Professional and clear; balance technical accuracy with readability."
206
+
207
+
208
+ def _build_examples(lower: str, role: str) -> Optional[List[Dict[str, str]]]:
209
+ """Return 1 few-shot example where applicable."""
210
+ if "react" in lower or "component" in lower:
211
+ return [
212
+ {
213
+ "input": "Create a Button component.",
214
+ "output": "```tsx\ninterface ButtonProps { label: string; onClick: () => void; }\nexport const Button = ({ label, onClick }: ButtonProps) => (\n <button onClick={onClick} className='px-4 py-2 bg-blue-600 text-white rounded'>{label}</button>\n);\n```",
215
+ }
216
+ ]
217
+ if "summarize" in lower or "summary" in lower:
218
+ return [
219
+ {
220
+ "input": "Summarize: 'The quick brown fox jumps over the lazy dog repeatedly.'",
221
+ "output": "A fox repeatedly jumps over a dog.",
222
+ }
223
+ ]
224
+ return None
225
+
226
+
227
+ def _render_raw_prompt(
228
+ role: str,
229
+ task: str,
230
+ input_fmt: str,
231
+ output_fmt: str,
232
+ constraints: List[str],
233
+ style: str,
234
+ safety: List[str],
235
+ examples: Optional[List[Dict[str, str]]] = None,
236
+ extra_context: Optional[str] = None,
237
+ ) -> str:
238
+ lines = [
239
+ f"## ROLE\nYou are a {role}.",
240
+ f"\n## TASK\n{task}",
241
+ f"\n## INPUT FORMAT\n{input_fmt}",
242
+ f"\n## OUTPUT FORMAT\n{output_fmt}",
243
+ "\n## CONSTRAINTS",
244
+ ]
245
+ for i, c in enumerate(constraints, 1):
246
+ lines.append(f"{i}. {c}")
247
+
248
+ lines.append(f"\n## STYLE & TONE\n{style}")
249
+
250
+ lines.append("\n## SAFETY GUIDELINES")
251
+ for i, s in enumerate(safety, 1):
252
+ lines.append(f"{i}. {s}")
253
+
254
+ if extra_context:
255
+ lines.append(f"\n## ADDITIONAL CONTEXT\n{extra_context}")
256
+
257
+ if examples:
258
+ lines.append("\n## FEW-SHOT EXAMPLES")
259
+ for ex in examples:
260
+ lines.append(f"**Input:** {ex['input']}")
261
+ lines.append(f"**Output:** {ex['output']}\n")
262
+
263
+ lines.append("\n---\n*Prompt generated by PromptForge — compatible with Google AI Studio.*")
264
+ return "\n".join(lines)
backend/requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi>=0.111.0
2
+ uvicorn[standard]>=0.29.0
3
+ pydantic>=2.7.0
4
+ httpx>=0.27.0
5
+ python-multipart>=0.0.9
6
+ pytest>=8.2.0
7
+ pytest-asyncio>=0.23.0
8
+ httpx>=0.27.0
backend/schemas.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PromptForge — Pydantic schemas for request/response validation.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import Any, Dict, List, Optional
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ # ---------------------------------------------------------------------------
12
+ # Enumerations
13
+ # ---------------------------------------------------------------------------
14
+
15
+ class OutputFormat(str, Enum):
16
+ text = "text"
17
+ json = "json"
18
+ both = "both"
19
+
20
+
21
+ class AIProvider(str, Enum):
22
+ none = "none"
23
+ huggingface = "huggingface"
24
+ google = "google"
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Request models
29
+ # ---------------------------------------------------------------------------
30
+
31
+ class GenerateRequest(BaseModel):
32
+ instruction: str = Field(
33
+ ...,
34
+ min_length=5,
35
+ max_length=8000,
36
+ description="Raw user instruction, description, or task to convert into a structured prompt.",
37
+ example="Generate a TypeScript React component with TailwindCSS and unit tests.",
38
+ )
39
+ output_format: OutputFormat = Field(
40
+ OutputFormat.both,
41
+ description="Desired output format: 'text', 'json', or 'both'.",
42
+ )
43
+ provider: AIProvider = Field(
44
+ AIProvider.none,
45
+ description="Optional AI provider to use for iterative enhancement.",
46
+ )
47
+ api_key: Optional[str] = Field(
48
+ None,
49
+ description="API key for the selected provider (never stored in logs).",
50
+ )
51
+ enhance: bool = Field(
52
+ False,
53
+ description="If True, call the selected AI provider to iteratively refine the prompt.",
54
+ )
55
+ extra_context: Optional[str] = Field(
56
+ None,
57
+ max_length=2000,
58
+ description="Optional additional context or constraints for the prompt.",
59
+ )
60
+
61
+
62
+ class ApproveRequest(BaseModel):
63
+ prompt_id: str = Field(..., description="ID of the pending prompt manifest to approve.")
64
+ edits: Optional[Dict[str, Any]] = Field(
65
+ None,
66
+ description="Optional user edits to apply before finalizing.",
67
+ )
68
+
69
+
70
+ class ExportRequest(BaseModel):
71
+ prompt_id: str = Field(..., description="ID of the finalized prompt to export.")
72
+ export_format: OutputFormat = Field(OutputFormat.json)
73
+
74
+
75
+ class RefineRequest(BaseModel):
76
+ prompt_id: str = Field(..., description="ID of a previously generated prompt to refine.")
77
+ feedback: str = Field(
78
+ ...,
79
+ min_length=3,
80
+ max_length=2000,
81
+ description="User feedback describing changes needed.",
82
+ )
83
+ provider: AIProvider = Field(AIProvider.none)
84
+ api_key: Optional[str] = Field(None)
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # Inner prompt structure
89
+ # ---------------------------------------------------------------------------
90
+
91
+ class StructuredPrompt(BaseModel):
92
+ role: str = Field(..., description="The persona or role the AI should adopt.")
93
+ task: str = Field(..., description="Clear description of the task to perform.")
94
+ input_format: str = Field(..., description="Expected input format and fields.")
95
+ output_format: str = Field(..., description="Expected output format and fields.")
96
+ constraints: List[str] = Field(default_factory=list, description="Hard constraints the AI must respect.")
97
+ style: str = Field(..., description="Tone, style, and voice guidelines.")
98
+ safety: List[str] = Field(default_factory=list, description="Safety and ethical guardrails.")
99
+ examples: Optional[List[Dict[str, str]]] = Field(None, description="Few-shot examples (input/output pairs).")
100
+ raw_prompt_text: str = Field(..., description="Formatted plain-text prompt ready for copy-paste into Google AI Studio.")
101
+
102
+
103
+ class PromptManifest(BaseModel):
104
+ prompt_id: str = Field(..., description="Unique identifier for this manifest.")
105
+ version: int = Field(1, description="Version counter for iterative refinement.")
106
+ created_at: datetime = Field(default_factory=datetime.utcnow)
107
+ instruction: str = Field(..., description="Original user instruction.")
108
+ status: str = Field("pending", description="pending | approved | exported")
109
+ structured_prompt: StructuredPrompt
110
+ enhancement_notes: Optional[str] = Field(None, description="Notes from AI enhancement pass.")
111
+
112
+
113
+ # ---------------------------------------------------------------------------
114
+ # Response models
115
+ # ---------------------------------------------------------------------------
116
+
117
+ class GenerateResponse(BaseModel):
118
+ success: bool
119
+ prompt_id: str
120
+ manifest: PromptManifest
121
+ message: str = "Manifest generated — awaiting approval."
122
+
123
+
124
+ class ApproveResponse(BaseModel):
125
+ success: bool
126
+ prompt_id: str
127
+ message: str
128
+ finalized_prompt: StructuredPrompt
129
+
130
+
131
+ class ExportResponse(BaseModel):
132
+ success: bool
133
+ prompt_id: str
134
+ data: Any # JSON dict or plain text string
135
+
136
+
137
+ class HistoryEntry(BaseModel):
138
+ prompt_id: str
139
+ version: int
140
+ created_at: datetime
141
+ instruction: str
142
+ status: str
143
+
144
+
145
+ class HistoryResponse(BaseModel):
146
+ total: int
147
+ entries: List[HistoryEntry]
backend/store.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PromptForge — In-memory store with optional JSON-file persistence.
3
+ In production, swap this for a proper database (PostgreSQL, SQLite, etc.).
4
+ """
5
+ from __future__ import annotations
6
+ import json
7
+ import os
8
+ import logging
9
+ from copy import deepcopy
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+ from typing import Dict, List, Optional
13
+
14
+ from schemas import PromptManifest, HistoryEntry
15
+
16
+ logger = logging.getLogger("promptforge.store")
17
+
18
+ _DB: Dict[str, PromptManifest] = {}
19
+ _LOG_DIR = Path(os.getenv("LOG_DIR", "logs"))
20
+ _LOG_DIR.mkdir(parents=True, exist_ok=True)
21
+ _PERSIST_FILE = _LOG_DIR / "prompt_history.json"
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # CRUD
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def save(manifest: PromptManifest) -> None:
29
+ _DB[manifest.prompt_id] = manifest
30
+ _persist()
31
+
32
+
33
+ def get(prompt_id: str) -> Optional[PromptManifest]:
34
+ return _DB.get(prompt_id)
35
+
36
+
37
+ def all_entries() -> List[HistoryEntry]:
38
+ return [
39
+ HistoryEntry(
40
+ prompt_id=m.prompt_id,
41
+ version=m.version,
42
+ created_at=m.created_at,
43
+ instruction=m.instruction,
44
+ status=m.status,
45
+ )
46
+ for m in sorted(_DB.values(), key=lambda x: x.created_at, reverse=True)
47
+ ]
48
+
49
+
50
+ def delete(prompt_id: str) -> bool:
51
+ if prompt_id in _DB:
52
+ del _DB[prompt_id]
53
+ _persist()
54
+ return True
55
+ return False
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Persistence
60
+ # ---------------------------------------------------------------------------
61
+
62
+ def _persist() -> None:
63
+ try:
64
+ data = [m.model_dump(mode="json") for m in _DB.values()]
65
+ _PERSIST_FILE.write_text(json.dumps(data, indent=2, default=str))
66
+ except Exception as exc:
67
+ logger.warning("Could not persist store: %s", exc)
68
+
69
+
70
+ def load_from_disk() -> None:
71
+ """Call once at startup to reload previous sessions."""
72
+ if not _PERSIST_FILE.exists():
73
+ return
74
+ try:
75
+ raw = json.loads(_PERSIST_FILE.read_text())
76
+ for entry in raw:
77
+ m = PromptManifest.model_validate(entry)
78
+ _DB[m.prompt_id] = m
79
+ logger.info("Loaded %d prompts from disk.", len(_DB))
80
+ except Exception as exc:
81
+ logger.warning("Could not load persisted store: %s", exc)
backend/tests/test_promptforge.py ADDED
@@ -0,0 +1,204 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PromptForge — Unit tests for prompt logic, schemas, and API routes.
3
+ Run with: pytest tests/ -v
4
+ """
5
+ from __future__ import annotations
6
+ import sys
7
+ import os
8
+
9
+ # Make backend importable
10
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
11
+
12
+ import pytest
13
+ from fastapi.testclient import TestClient
14
+
15
+ from prompt_logic import build_manifest, apply_edits, refine_with_feedback
16
+ from schemas import PromptManifest, GenerateRequest, OutputFormat, AIProvider
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Fixtures
21
+ # ---------------------------------------------------------------------------
22
+
23
+ @pytest.fixture
24
+ def client():
25
+ from main import app
26
+ return TestClient(app)
27
+
28
+
29
+ @pytest.fixture
30
+ def sample_instruction():
31
+ return "Generate a TypeScript React component with TailwindCSS and unit tests."
32
+
33
+
34
+ @pytest.fixture
35
+ def sample_manifest(sample_instruction):
36
+ return build_manifest(sample_instruction)
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # prompt_logic tests
41
+ # ---------------------------------------------------------------------------
42
+
43
+ class TestBuildManifest:
44
+ def test_returns_manifest_type(self, sample_manifest):
45
+ assert isinstance(sample_manifest, PromptManifest)
46
+
47
+ def test_status_is_pending(self, sample_manifest):
48
+ assert sample_manifest.status == "pending"
49
+
50
+ def test_version_is_1(self, sample_manifest):
51
+ assert sample_manifest.version == 1
52
+
53
+ def test_prompt_id_is_set(self, sample_manifest):
54
+ assert len(sample_manifest.prompt_id) == 36 # UUID4
55
+
56
+ def test_role_inferred_for_react(self, sample_manifest):
57
+ assert "Frontend" in sample_manifest.structured_prompt.role
58
+
59
+ def test_typescript_constraint_inferred(self, sample_manifest):
60
+ constraints = sample_manifest.structured_prompt.constraints
61
+ assert any("TypeScript" in c for c in constraints)
62
+
63
+ def test_tailwind_constraint_inferred(self, sample_manifest):
64
+ constraints = sample_manifest.structured_prompt.constraints
65
+ assert any("TailwindCSS" in c for c in constraints)
66
+
67
+ def test_raw_prompt_text_contains_role(self, sample_manifest):
68
+ raw = sample_manifest.structured_prompt.raw_prompt_text
69
+ assert "## ROLE" in raw
70
+
71
+ def test_raw_prompt_text_contains_task(self, sample_manifest):
72
+ raw = sample_manifest.structured_prompt.raw_prompt_text
73
+ assert "## TASK" in raw
74
+
75
+ def test_safety_guidelines_present(self, sample_manifest):
76
+ assert len(sample_manifest.structured_prompt.safety) >= 3
77
+
78
+ def test_extra_context_included(self):
79
+ m = build_manifest("Write a blog post.", extra_context="Keep it under 500 words.")
80
+ assert "500 words" in m.structured_prompt.raw_prompt_text
81
+
82
+ def test_summarize_instruction(self):
83
+ m = build_manifest("Summarize this document for me.")
84
+ assert "Summarizer" in m.structured_prompt.role
85
+
86
+ def test_unknown_instruction_gets_default_role(self):
87
+ m = build_manifest("Do something interesting.")
88
+ assert m.structured_prompt.role == "General AI Assistant"
89
+
90
+
91
+ class TestApplyEdits:
92
+ def test_apply_role_edit(self, sample_manifest):
93
+ edited = apply_edits(sample_manifest, {"role": "Principal Software Engineer"})
94
+ assert edited.structured_prompt.role == "Principal Software Engineer"
95
+
96
+ def test_status_becomes_approved(self, sample_manifest):
97
+ edited = apply_edits(sample_manifest, {"task": "Updated task."})
98
+ assert edited.status == "approved"
99
+
100
+ def test_raw_text_regenerated(self, sample_manifest):
101
+ edited = apply_edits(sample_manifest, {"role": "XYZ"})
102
+ assert "XYZ" in edited.structured_prompt.raw_prompt_text
103
+
104
+
105
+ class TestRefineWithFeedback:
106
+ def test_version_increments(self, sample_manifest):
107
+ refined = refine_with_feedback(sample_manifest, "Also add accessibility support.")
108
+ assert refined.version == 2
109
+
110
+ def test_same_prompt_id(self, sample_manifest):
111
+ refined = refine_with_feedback(sample_manifest, "Add dark mode support.")
112
+ assert refined.prompt_id == sample_manifest.prompt_id
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # API route tests
117
+ # ---------------------------------------------------------------------------
118
+
119
+ class TestGenerateRoute:
120
+ def test_generate_returns_200(self, client, sample_instruction):
121
+ resp = client.post("/api/generate", json={"instruction": sample_instruction})
122
+ assert resp.status_code == 200
123
+
124
+ def test_generate_response_has_prompt_id(self, client, sample_instruction):
125
+ resp = client.post("/api/generate", json={"instruction": sample_instruction})
126
+ data = resp.json()
127
+ assert "prompt_id" in data
128
+ assert len(data["prompt_id"]) == 36
129
+
130
+ def test_generate_response_has_manifest(self, client, sample_instruction):
131
+ resp = client.post("/api/generate", json={"instruction": sample_instruction})
132
+ data = resp.json()
133
+ assert "manifest" in data
134
+ assert data["manifest"]["status"] == "pending"
135
+
136
+ def test_generate_short_instruction_422(self, client):
137
+ resp = client.post("/api/generate", json={"instruction": "hi"})
138
+ assert resp.status_code == 422
139
+
140
+ def test_generate_missing_instruction_422(self, client):
141
+ resp = client.post("/api/generate", json={})
142
+ assert resp.status_code == 422
143
+
144
+
145
+ class TestApproveRoute:
146
+ def _generate(self, client, instruction="Build an API endpoint."):
147
+ resp = client.post("/api/generate", json={"instruction": instruction})
148
+ return resp.json()["prompt_id"]
149
+
150
+ def test_approve_returns_200(self, client):
151
+ pid = self._generate(client)
152
+ resp = client.post("/api/approve", json={"prompt_id": pid})
153
+ assert resp.status_code == 200
154
+
155
+ def test_approve_status_approved(self, client):
156
+ pid = self._generate(client)
157
+ resp = client.post("/api/approve", json={"prompt_id": pid})
158
+ assert resp.json()["success"] is True
159
+
160
+ def test_approve_unknown_id_404(self, client):
161
+ resp = client.post("/api/approve", json={"prompt_id": "00000000-0000-0000-0000-000000000000"})
162
+ assert resp.status_code == 404
163
+
164
+
165
+ class TestExportRoute:
166
+ def _generate_and_approve(self, client):
167
+ pid = client.post("/api/generate", json={"instruction": "Write a Python script."}).json()["prompt_id"]
168
+ client.post("/api/approve", json={"prompt_id": pid})
169
+ return pid
170
+
171
+ def test_export_json_200(self, client):
172
+ pid = self._generate_and_approve(client)
173
+ resp = client.post("/api/export", json={"prompt_id": pid, "export_format": "json"})
174
+ assert resp.status_code == 200
175
+ data = resp.json()
176
+ assert isinstance(data["data"], dict)
177
+
178
+ def test_export_text_200(self, client):
179
+ pid = self._generate_and_approve(client)
180
+ resp = client.post("/api/export", json={"prompt_id": pid, "export_format": "text"})
181
+ assert resp.status_code == 200
182
+ assert isinstance(resp.json()["data"], str)
183
+
184
+ def test_export_unapproved_400(self, client):
185
+ pid = client.post("/api/generate", json={"instruction": "Write a Python script again."}).json()["prompt_id"]
186
+ resp = client.post("/api/export", json={"prompt_id": pid, "export_format": "json"})
187
+ assert resp.status_code == 400
188
+
189
+
190
+ class TestHistoryRoute:
191
+ def test_history_returns_200(self, client):
192
+ resp = client.get("/api/history")
193
+ assert resp.status_code == 200
194
+
195
+ def test_history_has_total_field(self, client):
196
+ resp = client.get("/api/history")
197
+ assert "total" in resp.json()
198
+
199
+
200
+ class TestHealthRoute:
201
+ def test_health_ok(self, client):
202
+ resp = client.get("/health")
203
+ assert resp.status_code == 200
204
+ assert resp.json()["status"] == "ok"
docker-compose.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: "3.9"
2
+
3
+ services:
4
+ promptforge:
5
+ build: .
6
+ container_name: promptforge
7
+ restart: unless-stopped
8
+ ports:
9
+ - "8000:8000"
10
+ volumes:
11
+ - ./logs:/app/logs # Persist prompt history
12
+ environment:
13
+ # ── AI Provider keys (set in .env file, never commit) ──────────
14
+ # GOOGLE_API_KEY: set via .env or deployment secret manager
15
+ # HF_API_KEY: set via .env or deployment secret manager
16
+ LOG_DIR: /app/logs
17
+ env_file:
18
+ - .env # Create from .env.example
19
+ healthcheck:
20
+ test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
21
+ interval: 30s
22
+ timeout: 5s
23
+ retries: 3
frontend/client.js ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * PromptForge — client.js
3
+ * Handles all API communication and UI state transitions.
4
+ */
5
+
6
+ const API_BASE = ""; // Same origin; change to e.g. "http://localhost:8000" for dev
7
+ let currentPromptId = null; // Tracks the active manifest
8
+
9
+ // ── DOM refs ──────────────────────────────────────────────────────
10
+ const $ = id => document.getElementById(id);
11
+ const steps = {
12
+ input: $("step-input"),
13
+ manifest: $("step-manifest"),
14
+ finalized: $("step-finalized"),
15
+ refine: $("step-refine"),
16
+ };
17
+
18
+ // ── Utility ───────────────────────────────────────────────────────
19
+ function show(el) { el.classList.remove("hidden"); }
20
+ function hide(el) { el.classList.add("hidden"); }
21
+ function showStep(name) {
22
+ Object.values(steps).forEach(hide);
23
+ show(steps[name]);
24
+ }
25
+
26
+ function toast(msg, type = "info") {
27
+ const t = $("toast");
28
+ t.textContent = msg;
29
+ t.className = `toast ${type}`;
30
+ show(t);
31
+ setTimeout(() => hide(t), 3500);
32
+ }
33
+
34
+ function setLoading(btn, loading) {
35
+ btn.disabled = loading;
36
+ btn.dataset.originalText ??= btn.textContent;
37
+ btn.textContent = loading ? "⏳ Working…" : btn.dataset.originalText;
38
+ }
39
+
40
+ async function apiFetch(path, method = "GET", body = null) {
41
+ const opts = {
42
+ method,
43
+ headers: { "Content-Type": "application/json" },
44
+ };
45
+ if (body) opts.body = JSON.stringify(body);
46
+ const resp = await fetch(API_BASE + path, opts);
47
+ if (!resp.ok) {
48
+ const err = await resp.json().catch(() => ({ detail: resp.statusText }));
49
+ throw new Error(err.detail || "Unknown error");
50
+ }
51
+ return resp.json();
52
+ }
53
+
54
+ // ── Step 1: Generate ──────────────────────────────────────────────
55
+ $("btn-generate").addEventListener("click", async () => {
56
+ const instruction = $("instruction").value.trim();
57
+ if (!instruction || instruction.length < 5) {
58
+ toast("Please enter a meaningful instruction (min 5 characters).", "error");
59
+ return;
60
+ }
61
+
62
+ const provider = $("provider").value;
63
+ const apiKey = $("api-key").value.trim();
64
+ const extraCtx = $("extra-context").value.trim();
65
+ const enhance = provider !== "none" && !!apiKey;
66
+
67
+ const btn = $("btn-generate");
68
+ setLoading(btn, true);
69
+ try {
70
+ const data = await apiFetch("/api/generate", "POST", {
71
+ instruction,
72
+ output_format: "both",
73
+ provider,
74
+ api_key: apiKey || null,
75
+ enhance,
76
+ extra_context: extraCtx || null,
77
+ });
78
+
79
+ currentPromptId = data.prompt_id;
80
+ renderManifest(data.manifest);
81
+ show(steps.manifest);
82
+ steps.manifest.scrollIntoView({ behavior: "smooth" });
83
+ toast("✅ Manifest generated — review and approve.", "success");
84
+ } catch (e) {
85
+ toast(`Error: ${e.message}`, "error");
86
+ } finally {
87
+ setLoading(btn, false);
88
+ }
89
+ });
90
+
91
+ // ── Provider selector shows/hides API key field ───────────────────
92
+ $("provider").addEventListener("change", () => {
93
+ const group = $("api-key-group");
94
+ group.style.display = $("provider").value !== "none" ? "flex" : "none";
95
+ });
96
+
97
+ // ── Render manifest into editable grid ───────────────────────────
98
+ function renderManifest(manifest) {
99
+ const sp = manifest.structured_prompt;
100
+ const grid = $("manifest-grid");
101
+ grid.innerHTML = "";
102
+
103
+ const fields = [
104
+ { key: "role", label: "Role", value: sp.role, full: false },
105
+ { key: "style", label: "Style & Tone", value: sp.style, full: false },
106
+ { key: "task", label: "Task", value: sp.task, full: true },
107
+ { key: "input_format", label: "Input Format", value: sp.input_format, full: false },
108
+ { key: "output_format", label: "Output Format", value: sp.output_format, full: false },
109
+ { key: "constraints", label: "Constraints", value: sp.constraints.join("\n"), full: true },
110
+ { key: "safety", label: "Safety", value: sp.safety.join("\n"), full: true },
111
+ ];
112
+
113
+ fields.forEach(f => {
114
+ const div = document.createElement("div");
115
+ div.className = `manifest-field${f.full ? " full" : ""}`;
116
+ div.innerHTML = `<label>${f.label}</label>
117
+ <textarea id="field-${f.key}" rows="${f.full ? 3 : 2}">${escapeHtml(f.value)}</textarea>`;
118
+ grid.appendChild(div);
119
+ });
120
+
121
+ $("manifest-json").textContent = JSON.stringify(manifest, null, 2);
122
+ }
123
+
124
+ // ── Step 2/3: Approve ─────────────────────────────────────────────
125
+ $("btn-approve").addEventListener("click", async () => {
126
+ if (!currentPromptId) return;
127
+
128
+ // Collect edited field values
129
+ const edits = {};
130
+ ["role", "style", "task", "input_format", "output_format"].forEach(key => {
131
+ const el = $(`field-${key}`);
132
+ if (el) edits[key] = el.value.trim();
133
+ });
134
+ const cEl = $("field-constraints");
135
+ if (cEl) edits.constraints = cEl.value.trim().split("\n").filter(Boolean);
136
+ const sEl = $("field-safety");
137
+ if (sEl) edits.safety = sEl.value.trim().split("\n").filter(Boolean);
138
+
139
+ const btn = $("btn-approve");
140
+ setLoading(btn, true);
141
+ try {
142
+ const data = await apiFetch("/api/approve", "POST", {
143
+ prompt_id: currentPromptId,
144
+ edits,
145
+ });
146
+
147
+ renderFinalizedPrompt(data.finalized_prompt);
148
+ hide(steps.manifest);
149
+ show(steps.finalized);
150
+ steps.finalized.scrollIntoView({ behavior: "smooth" });
151
+ toast("✅ Prompt approved and finalized!", "success");
152
+ } catch (e) {
153
+ toast(`Approval failed: ${e.message}`, "error");
154
+ } finally {
155
+ setLoading(btn, false);
156
+ }
157
+ });
158
+
159
+ // ── Render finalized prompt ────────────────────────────────────────
160
+ function renderFinalizedPrompt(sp) {
161
+ $("finalized-text").textContent = sp.raw_prompt_text;
162
+ $("finalized-json").textContent = JSON.stringify(sp, null, 2);
163
+ }
164
+
165
+ // ── Tabs ──────────────────────────────────────────────────────────
166
+ document.querySelectorAll(".tab").forEach(tab => {
167
+ tab.addEventListener("click", () => {
168
+ const target = tab.dataset.tab;
169
+ document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
170
+ tab.classList.add("active");
171
+ document.querySelectorAll(".tab-panel").forEach(p => hide(p));
172
+ show($(`tab-${target}`));
173
+ });
174
+ });
175
+
176
+ // ── Copy buttons ──────────────────────────────────────────────────
177
+ document.querySelectorAll(".btn-copy").forEach(btn => {
178
+ btn.addEventListener("click", () => {
179
+ const pre = $(btn.dataset.target);
180
+ if (!pre) return;
181
+ navigator.clipboard.writeText(pre.textContent).then(() => {
182
+ const orig = btn.textContent;
183
+ btn.textContent = "✓ Copied!";
184
+ setTimeout(() => (btn.textContent = orig), 1800);
185
+ });
186
+ });
187
+ });
188
+
189
+ // ── Export ────────────────────────────────────────────────────────
190
+ async function exportPrompt(format) {
191
+ if (!currentPromptId) return;
192
+ try {
193
+ const data = await apiFetch("/api/export", "POST", {
194
+ prompt_id: currentPromptId,
195
+ export_format: format,
196
+ });
197
+ const content = typeof data.data === "string" ? data.data : JSON.stringify(data.data, null, 2);
198
+ const ext = format === "text" ? "txt" : "json";
199
+ downloadFile(`prompt_${currentPromptId.slice(0,8)}.${ext}`, content);
200
+ toast(`⬇ Exported as .${ext}`, "success");
201
+ } catch (e) {
202
+ toast(`Export failed: ${e.message}`, "error");
203
+ }
204
+ }
205
+
206
+ $("btn-export-json").addEventListener("click", () => exportPrompt("json"));
207
+ $("btn-export-txt").addEventListener("click", () => exportPrompt("text"));
208
+
209
+ function downloadFile(filename, content) {
210
+ const a = document.createElement("a");
211
+ a.href = URL.createObjectURL(new Blob([content], { type: "text/plain" }));
212
+ a.download = filename;
213
+ a.click();
214
+ URL.revokeObjectURL(a.href);
215
+ }
216
+
217
+ // ── Refine ────────────────────────────────────────────────────────
218
+ $("btn-refine").addEventListener("click", () => {
219
+ show(steps.refine);
220
+ steps.refine.scrollIntoView({ behavior: "smooth" });
221
+ });
222
+ $("btn-cancel-refine").addEventListener("click", () => hide(steps.refine));
223
+
224
+ $("btn-submit-refine").addEventListener("click", async () => {
225
+ if (!currentPromptId) return;
226
+ const feedback = $("feedback").value.trim();
227
+ if (!feedback) { toast("Please enter feedback.", "error"); return; }
228
+
229
+ const btn = $("btn-submit-refine");
230
+ setLoading(btn, true);
231
+ try {
232
+ const data = await apiFetch("/api/refine", "POST", {
233
+ prompt_id: currentPromptId,
234
+ feedback,
235
+ provider: $("provider").value,
236
+ api_key: $("api-key").value.trim() || null,
237
+ });
238
+
239
+ currentPromptId = data.prompt_id;
240
+ renderManifest(data.manifest);
241
+ hide(steps.finalized);
242
+ hide(steps.refine);
243
+ show(steps.manifest);
244
+ $("feedback").value = "";
245
+ steps.manifest.scrollIntoView({ behavior: "smooth" });
246
+ toast(`🔁 Refined to v${data.manifest.version} — please re-approve.`, "success");
247
+ } catch (e) {
248
+ toast(`Refine failed: ${e.message}`, "error");
249
+ } finally {
250
+ setLoading(btn, false);
251
+ }
252
+ });
253
+
254
+ // ── Reset / New ───────────────────────────────────────────────────
255
+ function resetAll() {
256
+ currentPromptId = null;
257
+ $("instruction").value = "";
258
+ $("extra-context").value = "";
259
+ $("feedback").value = "";
260
+ Object.values(steps).forEach(hide);
261
+ show(steps.input);
262
+ window.scrollTo({ top: 0, behavior: "smooth" });
263
+ }
264
+
265
+ $("btn-reset").addEventListener("click", resetAll);
266
+ $("btn-new").addEventListener("click", resetAll);
267
+
268
+ // ── History ────────────────────────────────────────────────────────
269
+ $("btn-load-history").addEventListener("click", loadHistory);
270
+
271
+ async function loadHistory() {
272
+ try {
273
+ const data = await apiFetch("/api/history");
274
+ const tbody = $("history-body");
275
+
276
+ if (data.total === 0) {
277
+ tbody.innerHTML = `<tr><td colspan="6" class="muted center">No prompts generated yet.</td></tr>`;
278
+ return;
279
+ }
280
+
281
+ tbody.innerHTML = data.entries.map(e => `
282
+ <tr>
283
+ <td><code>${e.prompt_id.slice(0, 8)}…</code></td>
284
+ <td>${escapeHtml(e.instruction.slice(0, 60))}${e.instruction.length > 60 ? "…" : ""}</td>
285
+ <td>v${e.version}</td>
286
+ <td><span class="status-badge status-${e.status}">${e.status}</span></td>
287
+ <td>${new Date(e.created_at).toLocaleString()}</td>
288
+ <td>
289
+ <button class="btn-sm btn-secondary" onclick="loadPromptById('${e.prompt_id}')">Load</button>
290
+ <button class="btn-sm btn-secondary" onclick="deletePrompt('${e.prompt_id}')">🗑</button>
291
+ </td>
292
+ </tr>`).join("");
293
+ } catch (e) {
294
+ toast(`Could not load history: ${e.message}`, "error");
295
+ }
296
+ }
297
+
298
+ window.loadPromptById = async function(id) {
299
+ try {
300
+ const manifest = await apiFetch(`/api/history/${id}`);
301
+ currentPromptId = manifest.prompt_id;
302
+ renderManifest(manifest);
303
+ show(steps.manifest);
304
+ steps.manifest.scrollIntoView({ behavior: "smooth" });
305
+ toast("Manifest loaded from history.", "success");
306
+ } catch (e) {
307
+ toast(`Load failed: ${e.message}`, "error");
308
+ }
309
+ };
310
+
311
+ window.deletePrompt = async function(id) {
312
+ if (!confirm("Delete this prompt from history?")) return;
313
+ try {
314
+ await apiFetch(`/api/history/${id}`, "DELETE");
315
+ toast("Prompt deleted.", "success");
316
+ loadHistory();
317
+ } catch (e) {
318
+ toast(`Delete failed: ${e.message}`, "error");
319
+ }
320
+ };
321
+
322
+ // ── Helpers ────────────────────────────────────────────────────────
323
+ function escapeHtml(str) {
324
+ return String(str)
325
+ .replace(/&/g, "&amp;")
326
+ .replace(/</g, "&lt;")
327
+ .replace(/>/g, "&gt;")
328
+ .replace(/"/g, "&quot;");
329
+ }
330
+
331
+ // ── Init ──────────────────────────────────────────────────────────
332
+ loadHistory();
frontend/index.html ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>PromptForge — Google AI Studio Prompt Generator</title>
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ </head>
9
+ <body>
10
+ <header>
11
+ <div class="header-inner">
12
+ <div class="logo">⚙️ PromptForge</div>
13
+ <span class="tagline">Structured prompts for Google AI Studio</span>
14
+ </div>
15
+ </header>
16
+
17
+ <main>
18
+ <!-- ── Step 0: Input ─────────────────────────────────────────── -->
19
+ <section id="step-input" class="card">
20
+ <h2><span class="step-badge">STEP 1</span> Enter Your Instruction</h2>
21
+
22
+ <label for="instruction">Raw instruction / task description</label>
23
+ <textarea id="instruction" rows="5"
24
+ placeholder="e.g. Generate a TypeScript React component with TailwindCSS and unit tests."></textarea>
25
+
26
+ <label for="extra-context">Additional context (optional)</label>
27
+ <textarea id="extra-context" rows="2"
28
+ placeholder="e.g. The component should be accessible and support dark mode."></textarea>
29
+
30
+ <div class="row-two">
31
+ <div class="field-group">
32
+ <label for="provider">AI Enhancement Provider</label>
33
+ <select id="provider">
34
+ <option value="none">None (local only)</option>
35
+ <option value="google">Google Gemini</option>
36
+ <option value="huggingface">Hugging Face</option>
37
+ </select>
38
+ </div>
39
+ <div class="field-group" id="api-key-group" style="display:none;">
40
+ <label for="api-key">API Key <span class="muted">(never stored)</span></label>
41
+ <input id="api-key" type="password" placeholder="Paste your API key here..." />
42
+ </div>
43
+ </div>
44
+
45
+ <button id="btn-generate" class="btn-primary">⚡ Generate Prompt Manifest</button>
46
+ </section>
47
+
48
+ <!-- ── Step 1: Manifest preview ──────────────────────────────── -->
49
+ <section id="step-manifest" class="card hidden">
50
+ <h2><span class="step-badge">STEP 2</span> Review Manifest</h2>
51
+ <p class="muted">Review the generated manifest. Edit any field below, then approve.</p>
52
+
53
+ <div id="manifest-grid" class="manifest-grid"></div>
54
+
55
+ <details>
56
+ <summary>📋 Raw JSON Manifest</summary>
57
+ <pre id="manifest-json"></pre>
58
+ </details>
59
+
60
+ <div class="action-row">
61
+ <button id="btn-approve" class="btn-primary">✅ Approve &amp; Finalize</button>
62
+ <button id="btn-reset" class="btn-secondary">↩ Start Over</button>
63
+ </div>
64
+ </section>
65
+
66
+ <!-- ── Step 2: Finalized prompt ──────────────────────────────── -->
67
+ <section id="step-finalized" class="card hidden">
68
+ <h2><span class="step-badge">STEP 3</span> Finalized Prompt</h2>
69
+ <p class="muted">Your structured prompt is ready. Copy it directly into Google AI Studio.</p>
70
+
71
+ <div class="tab-bar">
72
+ <button class="tab active" data-tab="text">📄 Plain Text</button>
73
+ <button class="tab" data-tab="json">{ } JSON</button>
74
+ </div>
75
+
76
+ <div id="tab-text" class="tab-panel">
77
+ <pre id="finalized-text"></pre>
78
+ <button class="btn-copy" data-target="finalized-text">📋 Copy</button>
79
+ </div>
80
+ <div id="tab-json" class="tab-panel hidden">
81
+ <pre id="finalized-json"></pre>
82
+ <button class="btn-copy" data-target="finalized-json">📋 Copy</button>
83
+ </div>
84
+
85
+ <div class="action-row">
86
+ <button id="btn-export-json" class="btn-secondary">⬇ Export JSON</button>
87
+ <button id="btn-export-txt" class="btn-secondary">⬇ Export Text</button>
88
+ <button id="btn-refine" class="btn-secondary">🔁 Refine with Feedback</button>
89
+ <button id="btn-new" class="btn-primary">➕ New Prompt</button>
90
+ </div>
91
+ </section>
92
+
93
+ <!-- ── Refine panel ───────────────────────────────────────────── -->
94
+ <section id="step-refine" class="card hidden">
95
+ <h2><span class="step-badge">STEP 5</span> Refine Prompt</h2>
96
+ <label for="feedback">Your feedback / change requests</label>
97
+ <textarea id="feedback" rows="3"
98
+ placeholder="e.g. Add accessibility constraints. Use React hooks only."></textarea>
99
+ <div class="action-row">
100
+ <button id="btn-submit-refine" class="btn-primary">🔁 Submit Refinement</button>
101
+ <button id="btn-cancel-refine" class="btn-secondary">Cancel</button>
102
+ </div>
103
+ </section>
104
+
105
+ <!-- ── History ────────────────────────────────────────────────── -->
106
+ <section id="section-history" class="card">
107
+ <h2>📜 Prompt History</h2>
108
+ <button id="btn-load-history" class="btn-secondary btn-sm">Refresh History</button>
109
+ <table id="history-table">
110
+ <thead>
111
+ <tr><th>ID</th><th>Instruction</th><th>Version</th><th>Status</th><th>Date</th><th></th></tr>
112
+ </thead>
113
+ <tbody id="history-body">
114
+ <tr><td colspan="6" class="muted center">Click "Refresh History" to load.</td></tr>
115
+ </tbody>
116
+ </table>
117
+ </section>
118
+ </main>
119
+
120
+ <!-- Toast notification -->
121
+ <div id="toast" class="toast hidden"></div>
122
+
123
+ <footer>
124
+ <p>PromptForge v1.0 · <a href="/docs" target="_blank">API Docs</a> · Built with FastAPI + Vanilla JS</p>
125
+ </footer>
126
+
127
+ <script src="/static/client.js"></script>
128
+ </body>
129
+ </html>
frontend/style.css ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* PromptForge — style.css */
2
+
3
+ :root {
4
+ --bg: #0f1117;
5
+ --surface: #1a1d27;
6
+ --surface2: #22263a;
7
+ --border: #2e3350;
8
+ --accent: #5b8df6;
9
+ --accent-hover: #3d6fe0;
10
+ --success: #34d399;
11
+ --warning: #fbbf24;
12
+ --danger: #f87171;
13
+ --text: #e4e7f0;
14
+ --text-muted: #7c84a3;
15
+ --radius: 10px;
16
+ --font: 'Inter', system-ui, sans-serif;
17
+ --mono: 'Fira Code', 'Cascadia Code', monospace;
18
+ }
19
+
20
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
21
+
22
+ body {
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: var(--font);
26
+ font-size: 15px;
27
+ line-height: 1.6;
28
+ min-height: 100vh;
29
+ }
30
+
31
+ /* ── Header ──────────────────────────────────────────────────── */
32
+ header {
33
+ background: linear-gradient(135deg, #1e2336 0%, #151824 100%);
34
+ border-bottom: 1px solid var(--border);
35
+ padding: 14px 24px;
36
+ position: sticky;
37
+ top: 0;
38
+ z-index: 100;
39
+ backdrop-filter: blur(6px);
40
+ }
41
+ .header-inner { display: flex; align-items: center; gap: 16px; max-width: 960px; margin: 0 auto; }
42
+ .logo { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.3px; }
43
+ .tagline { color: var(--text-muted); font-size: 0.85rem; }
44
+
45
+ /* ── Main layout ──────────────────────────────────────────────── */
46
+ main {
47
+ max-width: 960px;
48
+ margin: 32px auto;
49
+ padding: 0 20px;
50
+ display: flex;
51
+ flex-direction: column;
52
+ gap: 24px;
53
+ }
54
+
55
+ /* ── Cards ──────────────────────────────────────────────────────── */
56
+ .card {
57
+ background: var(--surface);
58
+ border: 1px solid var(--border);
59
+ border-radius: var(--radius);
60
+ padding: 28px 30px;
61
+ }
62
+ .card h2 {
63
+ font-size: 1.05rem;
64
+ font-weight: 600;
65
+ margin-bottom: 18px;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 10px;
69
+ }
70
+ .step-badge {
71
+ background: var(--accent);
72
+ color: #fff;
73
+ font-size: 0.65rem;
74
+ font-weight: 700;
75
+ letter-spacing: 0.5px;
76
+ padding: 3px 8px;
77
+ border-radius: 20px;
78
+ }
79
+
80
+ /* ── Form elements ─────────────────────────────────────────────── */
81
+ label {
82
+ display: block;
83
+ margin-bottom: 6px;
84
+ font-size: 0.875rem;
85
+ color: var(--text-muted);
86
+ font-weight: 500;
87
+ }
88
+ textarea, input[type="password"], select {
89
+ width: 100%;
90
+ background: var(--surface2);
91
+ border: 1px solid var(--border);
92
+ border-radius: 7px;
93
+ color: var(--text);
94
+ font-family: var(--font);
95
+ font-size: 0.9rem;
96
+ padding: 10px 14px;
97
+ resize: vertical;
98
+ transition: border-color 0.2s;
99
+ margin-bottom: 16px;
100
+ }
101
+ textarea:focus, input:focus, select:focus {
102
+ outline: none;
103
+ border-color: var(--accent);
104
+ }
105
+ select option { background: var(--surface2); }
106
+
107
+ .row-two {
108
+ display: grid;
109
+ grid-template-columns: 1fr 1fr;
110
+ gap: 16px;
111
+ }
112
+ .field-group { display: flex; flex-direction: column; }
113
+ .field-group label { margin-bottom: 6px; }
114
+ .field-group textarea,
115
+ .field-group input,
116
+ .field-group select { margin-bottom: 0; }
117
+
118
+ /* ── Buttons ────────────────────────────────────────────────────── */
119
+ .btn-primary, .btn-secondary {
120
+ display: inline-flex;
121
+ align-items: center;
122
+ gap: 6px;
123
+ border: none;
124
+ border-radius: 7px;
125
+ cursor: pointer;
126
+ font-family: var(--font);
127
+ font-size: 0.9rem;
128
+ font-weight: 600;
129
+ padding: 10px 20px;
130
+ transition: background 0.2s, transform 0.1s;
131
+ }
132
+ .btn-primary {
133
+ background: var(--accent);
134
+ color: #fff;
135
+ }
136
+ .btn-primary:hover { background: var(--accent-hover); transform: translateY(-1px); }
137
+ .btn-primary:active { transform: translateY(0); }
138
+
139
+ .btn-secondary {
140
+ background: var(--surface2);
141
+ color: var(--text);
142
+ border: 1px solid var(--border);
143
+ }
144
+ .btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
145
+
146
+ .btn-sm { font-size: 0.8rem; padding: 7px 14px; }
147
+
148
+ .btn-copy {
149
+ background: var(--surface2);
150
+ border: 1px solid var(--border);
151
+ border-radius: 6px;
152
+ color: var(--text-muted);
153
+ cursor: pointer;
154
+ font-size: 0.8rem;
155
+ margin-top: 8px;
156
+ padding: 6px 14px;
157
+ transition: color 0.2s;
158
+ }
159
+ .btn-copy:hover { color: var(--success); border-color: var(--success); }
160
+
161
+ .action-row {
162
+ display: flex;
163
+ flex-wrap: wrap;
164
+ gap: 10px;
165
+ margin-top: 20px;
166
+ }
167
+
168
+ /* ── Manifest grid ──────────────────────────────────────────────── */
169
+ .manifest-grid {
170
+ display: grid;
171
+ grid-template-columns: 1fr 1fr;
172
+ gap: 16px;
173
+ margin-bottom: 20px;
174
+ }
175
+ .manifest-field { display: flex; flex-direction: column; }
176
+ .manifest-field.full { grid-column: 1 / -1; }
177
+ .manifest-field label { font-size: 0.78rem; color: var(--accent); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 4px; }
178
+ .manifest-field textarea,
179
+ .manifest-field input {
180
+ margin-bottom: 0;
181
+ font-size: 0.85rem;
182
+ min-height: 70px;
183
+ }
184
+
185
+ /* ── Tabs ───────────────────────────────────────────────────────── */
186
+ .tab-bar { display: flex; gap: 4px; margin-bottom: 14px; }
187
+ .tab {
188
+ background: none;
189
+ border: 1px solid var(--border);
190
+ border-radius: 6px;
191
+ color: var(--text-muted);
192
+ cursor: pointer;
193
+ font-family: var(--font);
194
+ font-size: 0.85rem;
195
+ padding: 7px 16px;
196
+ transition: all 0.2s;
197
+ }
198
+ .tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
199
+
200
+ /* ── Pre / code blocks ──────────────────────────────────────────── */
201
+ pre {
202
+ background: #0a0c14;
203
+ border: 1px solid var(--border);
204
+ border-radius: 8px;
205
+ color: #c9d1d9;
206
+ font-family: var(--mono);
207
+ font-size: 0.82rem;
208
+ line-height: 1.5;
209
+ overflow-x: auto;
210
+ padding: 16px 18px;
211
+ white-space: pre-wrap;
212
+ word-break: break-word;
213
+ }
214
+
215
+ details summary {
216
+ cursor: pointer;
217
+ color: var(--text-muted);
218
+ font-size: 0.85rem;
219
+ margin-bottom: 10px;
220
+ user-select: none;
221
+ }
222
+ details[open] summary { color: var(--text); }
223
+
224
+ /* ── History table ──────────────────────────────────────────────── */
225
+ table { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 0.85rem; }
226
+ th { color: var(--text-muted); font-weight: 600; padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--border); }
227
+ td { padding: 9px 10px; border-bottom: 1px solid #1e2236; vertical-align: middle; }
228
+ tr:hover td { background: var(--surface2); }
229
+ td.center { text-align: center; }
230
+
231
+ .status-badge {
232
+ border-radius: 20px;
233
+ font-size: 0.72rem;
234
+ font-weight: 700;
235
+ letter-spacing: 0.3px;
236
+ padding: 3px 9px;
237
+ text-transform: uppercase;
238
+ }
239
+ .status-pending { background: #2d2a1a; color: var(--warning); }
240
+ .status-approved { background: #162820; color: var(--success); }
241
+ .status-exported { background: #1a1f36; color: var(--accent); }
242
+
243
+ /* ── Utility ────────────────────────────────────────────────────── */
244
+ .hidden { display: none !important; }
245
+ .muted { color: var(--text-muted); font-size: 0.875rem; margin-bottom: 12px; }
246
+
247
+ /* ── Toast ──────────────────────────────────────────────────────── */
248
+ .toast {
249
+ position: fixed;
250
+ bottom: 28px;
251
+ right: 28px;
252
+ background: var(--surface2);
253
+ border: 1px solid var(--border);
254
+ border-radius: 9px;
255
+ box-shadow: 0 4px 24px rgba(0,0,0,0.4);
256
+ color: var(--text);
257
+ font-size: 0.875rem;
258
+ font-weight: 500;
259
+ max-width: 340px;
260
+ padding: 13px 18px;
261
+ z-index: 999;
262
+ animation: slideIn 0.25s ease;
263
+ }
264
+ .toast.success { border-color: var(--success); color: var(--success); }
265
+ .toast.error { border-color: var(--danger); color: var(--danger); }
266
+ @keyframes slideIn { from { opacity:0; transform: translateY(10px); } to { opacity:1; transform: translateY(0); } }
267
+
268
+ /* ── Footer ──────────────────────────────────────────────────────── */
269
+ footer {
270
+ text-align: center;
271
+ color: var(--text-muted);
272
+ font-size: 0.8rem;
273
+ padding: 24px 0 32px;
274
+ }
275
+ footer a { color: var(--accent); text-decoration: none; }
276
+ footer a:hover { text-decoration: underline; }
277
+
278
+ /* ── Responsive ──────────────────────────────────────────────────── */
279
+ @media (max-width: 640px) {
280
+ .row-two, .manifest-grid { grid-template-columns: 1fr; }
281
+ .manifest-field.full { grid-column: 1; }
282
+ .card { padding: 20px 16px; }
283
+ main { margin: 16px auto; }
284
+ }
promptforge.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:db09504dcb45573eda133e793bed9b319582e61ce7b6688827dd656d5f2f2cbf
3
+ size 27489
upload.bat ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @echo off
2
+ echo ================================================
3
+ echo PromptForge - Hugging Face Uploader
4
+ echo ================================================
5
+ echo.
6
+
7
+ :: Check Python
8
+ python --version >nul 2>&1
9
+ if errorlevel 1 (
10
+ echo [ERROR] Python not found. Install from https://python.org
11
+ pause
12
+ exit /b 1
13
+ )
14
+
15
+ :: Install huggingface_hub if needed
16
+ echo [1/2] Checking huggingface_hub...
17
+ pip show huggingface_hub >nul 2>&1
18
+ if errorlevel 1 (
19
+ echo Installing huggingface_hub...
20
+ pip install huggingface_hub
21
+ )
22
+
23
+ :: Run uploader
24
+ echo [2/2] Running uploader...
25
+ echo.
26
+ python upload_to_hf.py
27
+
28
+ pause