Really-amin commited on
Commit
133cc34
·
verified ·
1 Parent(s): 37e5459

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

Browse files
Files changed (8) hide show
  1. .env.example +1 -1
  2. README.md +1 -11
  3. backend/main.py +3 -2
  4. frontend/client.js +335 -14
  5. frontend/index.html +60 -44
  6. frontend/style.css +193 -184
  7. promptforge_fixed.zip +3 -0
  8. upload.zip +3 -0
.env.example CHANGED
@@ -12,5 +12,5 @@ HF_API_KEY=your_huggingface_api_key_here
12
 
13
  # ── Server settings ────────────────────────────────────────────────
14
  HOST=0.0.0.0
15
- PORT=7860
16
  LOG_DIR=./logs
 
12
 
13
  # ── Server settings ────────────────────────────────────────────────
14
  HOST=0.0.0.0
15
+ PORT=8000
16
  LOG_DIR=./logs
README.md CHANGED
@@ -1,13 +1,3 @@
1
- ---
2
- title: Gooleaiprompt
3
- sdk: docker
4
- emoji: 📚
5
- colorFrom: indigo
6
- colorTo: indigo
7
- pinned: true
8
- thumbnail: >-
9
- https://cdn-uploads.huggingface.co/production/uploads/66367933cc7af105efbcd2dc/BhPnIDIsrbGB8uOsUZUAX.png
10
- ---
11
  # ⚙️ PromptForge
12
 
13
  **PromptForge** is a cloud-ready FastAPI service that converts raw user instructions into
@@ -261,4 +251,4 @@ export const Button = ({ label, onClick }: ButtonProps) => (
261
 
262
  ## 📝 License
263
 
264
- MIT — use freely, modify as needed.
 
 
 
 
 
 
 
 
 
 
 
1
  # ⚙️ PromptForge
2
 
3
  **PromptForge** is a cloud-ready FastAPI service that converts raw user instructions into
 
251
 
252
  ## 📝 License
253
 
254
+ MIT — use freely, modify as needed.
backend/main.py CHANGED
@@ -1,6 +1,6 @@
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
@@ -62,7 +62,8 @@ if _FRONTEND_DIR.exists():
62
  @app.on_event("startup")
63
  async def _startup() -> None:
64
  store.load_from_disk()
65
- logger.info("PromptForge started. Visit http://localhost:7860")
 
66
 
67
 
68
  # ---------------------------------------------------------------------------
 
1
  """
2
  PromptForge — FastAPI server
3
+ Run: uvicorn main:app --reload --host 0.0.0.0 --port 7860
4
  """
5
  from __future__ import annotations
6
  import logging
 
62
  @app.on_event("startup")
63
  async def _startup() -> None:
64
  store.load_from_disk()
65
+ port = os.environ.get("PORT", "7860")
66
+ logger.info("PromptForge started. Visit http://localhost:%s", port)
67
 
68
 
69
  # ---------------------------------------------------------------------------
frontend/client.js CHANGED
@@ -1,23 +1,344 @@
1
  /**
2
- * PromptForge — client.js v2.0
3
- * Custom cursor, toast system, step progress, full API flow.
4
  */
5
- const API = "";
 
6
  let currentPromptId = null;
7
  const $ = id => document.getElementById(id);
8
  const show = el => el?.classList.remove("hidden");
9
  const hide = el => el?.classList.add("hidden");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- /* Custom Cursor */
12
- const dot = $("cursor-dot"), ring = $("cursor-ring");
13
- let mx=0,my=0,rx=0,ry=0;
14
- document.addEventListener("mousemove", e => {
15
- mx=e.clientX; my=e.clientY;
16
- dot.style.left=mx+"px"; dot.style.top=my+"px";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  });
18
- (function animRing(){ rx+=(mx-rx)*.14; ry+=(my-ry)*.14;
19
- ring.style.left=rx+"px"; ring.style.top=ry+"px"; requestAnimationFrame(animRing); })();
20
- document.querySelectorAll("button,a,select,textarea,input,summary").forEach(el=>{
21
- el.addEventListener("mouseenter",()=>{ dot.style.width="14px";dot.style.height="14px";dot.style.background="var(--magenta)";ring.style.width="54px";ring.style.height="54px";ring.style.borderColor="rgba(247,37,133,.45)"; });
22
- el.addEventListener("mouseleave",()=>{ dot.style.width="8px";dot.style.height="8px";dot.style.background="var(--cyan)";ring.style.width="36px";ring.style.height="36px";ring.style.borderColor="rgba(0,245,212,.45)"; });
 
 
 
 
 
 
 
23
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * PromptForge — client.js v3.0
3
+ * Clean modern UI: API key always visible, proper provider toggle, port 7860
4
  */
5
+
6
+ const API = ""; // Relative URL — works on any port (7860 for HuggingFace)
7
  let currentPromptId = null;
8
  const $ = id => document.getElementById(id);
9
  const show = el => el?.classList.remove("hidden");
10
  const hide = el => el?.classList.add("hidden");
11
+ const esc = s => String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
12
+
13
+ /* ── API Key Panel ──────────────────────────────────────────────── */
14
+ const providerSelect = $("provider");
15
+ const apiKeyInput = $("api-key");
16
+ const apiKeyHint = $("api-key-hint");
17
+ const apiStatusDot = $("api-status-dot");
18
+ const apiStatusText = $("api-status-text");
19
+
20
+ const PROVIDER_LABELS = {
21
+ none: { hint:"(no key needed)", placeholder:"Not required for local engine" },
22
+ google: { hint:"Google Gemini key", placeholder:"AIza… (get it at aistudio.google.com/apikey)" },
23
+ huggingface: { hint:"Hugging Face token", placeholder:"hf_… (get it at huggingface.co/settings/tokens)" },
24
+ };
25
+
26
+ function updateApiKeyPanel() {
27
+ const p = providerSelect.value;
28
+ const info = PROVIDER_LABELS[p] || PROVIDER_LABELS.none;
29
+ apiKeyHint.textContent = info.hint;
30
+ if (p === "none") {
31
+ apiKeyInput.disabled = true;
32
+ apiKeyInput.value = "";
33
+ apiKeyInput.placeholder = info.placeholder;
34
+ apiStatusDot.classList.remove("set");
35
+ apiStatusText.textContent = "Not needed — local engine selected";
36
+ } else {
37
+ apiKeyInput.disabled = false;
38
+ apiKeyInput.placeholder = info.placeholder;
39
+ updateApiKeyStatus();
40
+ }
41
+ }
42
+
43
+ function updateApiKeyStatus() {
44
+ const key = apiKeyInput.value.trim();
45
+ if (!key) {
46
+ apiStatusDot.classList.remove("set");
47
+ apiStatusText.textContent = "No key entered — AI enhancement disabled";
48
+ } else if (key.length < 10) {
49
+ apiStatusDot.classList.remove("set");
50
+ apiStatusText.textContent = "Key too short — check it";
51
+ } else {
52
+ apiStatusDot.classList.add("set");
53
+ apiStatusText.textContent = "Key is set ✓ — AI enhancement enabled";
54
+ }
55
+ }
56
+
57
+ providerSelect.addEventListener("change", updateApiKeyPanel);
58
+ apiKeyInput.addEventListener("input", updateApiKeyStatus);
59
+ updateApiKeyPanel(); // init state on load
60
+
61
+ /* Show/hide key toggle */
62
+ $("btn-toggle-key").addEventListener("click", () => {
63
+ const isPassword = apiKeyInput.type === "password";
64
+ apiKeyInput.type = isPassword ? "text" : "password";
65
+ $("btn-toggle-key").textContent = isPassword ? "🙈" : "👁";
66
+ });
67
 
68
+ /* ── Toast ──────────────────────────────────────────────────────── */
69
+ function toast(msg, type = "info") {
70
+ const icons = { success:"✅", error:"❌", info:"💡", warn:"⚠️" };
71
+ const c = $("toast-container");
72
+ const t = document.createElement("div");
73
+ t.className = `toast ${type}`;
74
+ t.innerHTML = `<div class="toast-icon">${icons[type]||"💡"}</div><span>${msg}</span>`;
75
+ c.appendChild(t);
76
+ setTimeout(() => {
77
+ t.classList.add("leaving");
78
+ t.addEventListener("animationend", () => t.remove());
79
+ }, 4000);
80
+ }
81
+
82
+ /* ── Step Progress ──────────────────────────────────────────────── */
83
+ function setStep(n) {
84
+ for (let i = 1; i <= 5; i++) {
85
+ const s = $(`pstep-${i}`); if (!s) continue;
86
+ s.classList.remove("active", "done");
87
+ if (i < n) s.classList.add("done");
88
+ if (i === n) s.classList.add("active");
89
+ }
90
+ for (let i = 1; i <= 4; i++) {
91
+ const l = $(`pline-${i}`); if (!l) continue;
92
+ l.classList.toggle("filled", i < n);
93
+ }
94
+ }
95
+
96
+ /* ── Tab Switcher ───────────────────────────────────────────────── */
97
+ document.querySelectorAll(".tab").forEach(tab => {
98
+ tab.addEventListener("click", () => {
99
+ const name = tab.dataset.tab;
100
+ document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
101
+ tab.classList.add("active");
102
+ document.querySelectorAll(".tab-panel").forEach(p => hide(p));
103
+ show($(`tab-${name}`));
104
+ });
105
  });
106
+
107
+ /* ── Copy Buttons ───────────────────────────────────────────────── */
108
+ document.addEventListener("click", e => {
109
+ const btn = e.target.closest(".btn-copy");
110
+ if (!btn) return;
111
+ const target = $(btn.dataset.target);
112
+ if (!target) return;
113
+ navigator.clipboard.writeText(target.textContent).then(() => {
114
+ btn.textContent = "✅ Copied!";
115
+ btn.classList.add("copied");
116
+ setTimeout(() => { btn.classList.remove("copied"); btn.textContent = "📋 Copy to Clipboard"; }, 2200);
117
+ });
118
  });
119
+
120
+ /* ── Loading State ──────────────────────────────────────────────── */
121
+ function setLoading(btn, on) {
122
+ btn.disabled = on;
123
+ btn._orig = btn._orig || btn.innerHTML;
124
+ btn.innerHTML = on ? `<span class="spinner"></span> Working…` : btn._orig;
125
+ }
126
+
127
+ /* ── API Fetch ──────────────────────────────────────────────────── */
128
+ async function apiFetch(path, method = "GET", body = null) {
129
+ const opts = { method, headers: { "Content-Type": "application/json" } };
130
+ if (body) opts.body = JSON.stringify(body);
131
+ const r = await fetch(API + path, opts);
132
+ if (!r.ok) {
133
+ const e = await r.json().catch(() => ({ detail: r.statusText }));
134
+ throw new Error(e.detail || "Request failed");
135
+ }
136
+ return r.json();
137
+ }
138
+
139
+ /* ── STEP 1: Generate ───────────────────────────────────────────── */
140
+ $("btn-generate").addEventListener("click", async () => {
141
+ const instruction = $("instruction").value.trim();
142
+ if (!instruction || instruction.length < 5) {
143
+ toast("Please enter a meaningful instruction (at least 5 characters).", "error");
144
+ return;
145
+ }
146
+ const btn = $("btn-generate");
147
+ setLoading(btn, true);
148
+ try {
149
+ const provider = providerSelect.value;
150
+ const apiKey = apiKeyInput.value.trim() || null;
151
+ const enhance = provider !== "none" && !!apiKey;
152
+
153
+ const data = await apiFetch("/api/generate", "POST", {
154
+ instruction,
155
+ output_format: "both",
156
+ provider,
157
+ api_key: apiKey,
158
+ enhance,
159
+ extra_context: $("extra-context").value.trim() || null,
160
+ });
161
+
162
+ currentPromptId = data.prompt_id;
163
+ renderManifest(data.manifest);
164
+ hide($("step-input"));
165
+ show($("step-manifest"));
166
+ $("step-manifest").scrollIntoView({ behavior: "smooth", block: "start" });
167
+ setStep(2);
168
+ toast(enhance ? "Manifest generated & AI-enhanced! ✨" : "Manifest generated — review and approve!", "success");
169
+ } catch (e) {
170
+ toast(`Error: ${e.message}`, "error");
171
+ } finally {
172
+ setLoading(btn, false);
173
+ }
174
+ });
175
+
176
+ function renderManifest(manifest) {
177
+ const sp = manifest.structured_prompt;
178
+ const grid = $("manifest-grid");
179
+ grid.innerHTML = "";
180
+ [
181
+ { key:"role", label:"Role", value:sp.role, full:false },
182
+ { key:"style", label:"Style & Tone", value:sp.style, full:false },
183
+ { key:"task", label:"Task", value:sp.task, full:true },
184
+ { key:"input_format", label:"Input Format", value:sp.input_format, full:false },
185
+ { key:"output_format",label:"Output Format", value:sp.output_format, full:false },
186
+ { key:"constraints", label:"Constraints", value:sp.constraints.join("\n"), full:true },
187
+ { key:"safety", label:"Safety", value:sp.safety.join("\n"), full:true },
188
+ ].forEach(f => {
189
+ const d = document.createElement("div");
190
+ d.className = `manifest-field${f.full ? " full" : ""}`;
191
+ d.innerHTML = `<label>${esc(f.label)}</label><textarea id="field-${f.key}" rows="${f.full ? 3 : 2}">${esc(f.value)}</textarea>`;
192
+ grid.appendChild(d);
193
+ });
194
+ $("manifest-json").textContent = JSON.stringify(manifest, null, 2);
195
+ }
196
+
197
+ /* ── STEP 2: Approve ────────────────────────────────────────────── */
198
+ $("btn-approve").addEventListener("click", async () => {
199
+ if (!currentPromptId) return;
200
+ const edits = {};
201
+ ["role","style","task","input_format","output_format"].forEach(k => {
202
+ const el = $(`field-${k}`); if (el) edits[k] = el.value.trim();
203
+ });
204
+ const cEl = $("field-constraints"); if (cEl) edits.constraints = cEl.value.trim().split("\n").filter(Boolean);
205
+ const sEl = $("field-safety"); if (sEl) edits.safety = sEl.value.trim().split("\n").filter(Boolean);
206
+ const btn = $("btn-approve");
207
+ setLoading(btn, true);
208
+ try {
209
+ const data = await apiFetch("/api/approve", "POST", { prompt_id: currentPromptId, edits });
210
+ renderFinalized(data.finalized_prompt);
211
+ hide($("step-manifest"));
212
+ show($("step-finalized"));
213
+ $("step-finalized").scrollIntoView({ behavior: "smooth", block: "start" });
214
+ setStep(3);
215
+ toast("Prompt approved and finalized! 🎉", "success");
216
+ } catch (e) {
217
+ toast(`Approval failed: ${e.message}`, "error");
218
+ } finally {
219
+ setLoading(btn, false);
220
+ }
221
+ });
222
+
223
+ function renderFinalized(sp) {
224
+ $("finalized-text").textContent = sp.raw_prompt_text;
225
+ $("finalized-json").textContent = JSON.stringify(sp, null, 2);
226
+ }
227
+
228
+ /* ── STEP 4: Export ─────────────────────────────────────────────── */
229
+ async function doExport(format) {
230
+ if (!currentPromptId) return;
231
+ try {
232
+ const data = await apiFetch("/api/export", "POST", { prompt_id: currentPromptId, export_format: format });
233
+ const content = format === "json" ? JSON.stringify(data.data, null, 2) : String(data.data);
234
+ const blob = new Blob([content], { type: format === "json" ? "application/json" : "text/plain" });
235
+ const url = URL.createObjectURL(blob);
236
+ const a = Object.assign(document.createElement("a"), { href:url, download:`prompt-${currentPromptId}.${format === "json" ? "json" : "txt"}` });
237
+ a.click(); URL.revokeObjectURL(url);
238
+ setStep(4);
239
+ toast(`Exported as ${format.toUpperCase()}!`, "success");
240
+ } catch (e) {
241
+ toast(`Export failed: ${e.message}`, "error");
242
+ }
243
+ }
244
+ $("btn-export-json").addEventListener("click", () => doExport("json"));
245
+ $("btn-export-txt" ).addEventListener("click", () => doExport("text"));
246
+
247
+ /* ── STEP 5: Refine ─────────────────────────────────────────────── */
248
+ $("btn-refine").addEventListener("click", () => {
249
+ hide($("step-finalized"));
250
+ show($("step-refine"));
251
+ $("step-refine").scrollIntoView({ behavior:"smooth", block:"start" });
252
+ setStep(5);
253
+ });
254
+ $("btn-cancel-refine").addEventListener("click", () => {
255
+ hide($("step-refine"));
256
+ show($("step-finalized"));
257
+ setStep(3);
258
+ });
259
+ $("btn-submit-refine").addEventListener("click", async () => {
260
+ const feedback = $("feedback").value.trim();
261
+ if (!feedback) { toast("Please describe what you want to change.", "error"); return; }
262
+ const btn = $("btn-submit-refine");
263
+ setLoading(btn, true);
264
+ try {
265
+ const provider = providerSelect.value;
266
+ const apiKey = apiKeyInput.value.trim() || null;
267
+ const data = await apiFetch("/api/refine", "POST", {
268
+ prompt_id: currentPromptId, feedback, provider, api_key: apiKey,
269
+ });
270
+ currentPromptId = data.prompt_id;
271
+ renderManifest(data.manifest);
272
+ hide($("step-refine"));
273
+ show($("step-manifest"));
274
+ $("step-manifest").scrollIntoView({ behavior:"smooth", block:"start" });
275
+ setStep(2);
276
+ toast(`Refined to ${data.message || "new version"} — review and approve!`, "success");
277
+ } catch (e) {
278
+ toast(`Refinement failed: ${e.message}`, "error");
279
+ } finally {
280
+ setLoading(btn, false);
281
+ }
282
+ });
283
+
284
+ /* ── Reset ──────────────────────────────────────────────────────── */
285
+ $("btn-reset").addEventListener("click", () => {
286
+ hide($("step-manifest"));
287
+ show($("step-input"));
288
+ $("instruction").value = "";
289
+ $("extra-context").value = "";
290
+ currentPromptId = null;
291
+ setStep(1);
292
+ toast("Reset — start a new prompt.", "info");
293
+ });
294
+ $("btn-new")?.addEventListener("click", () => {
295
+ hide($("step-finalized"));
296
+ show($("step-input"));
297
+ $("instruction").value = "";
298
+ $("extra-context").value = "";
299
+ currentPromptId = null;
300
+ setStep(1);
301
+ $("step-input").scrollIntoView({ behavior:"smooth", block:"start" });
302
+ });
303
+
304
+ /* ── History ────────────────────────────────────────────────────── */
305
+ $("btn-load-history").addEventListener("click", loadHistory);
306
+ async function loadHistory() {
307
+ const btn = $("btn-load-history");
308
+ setLoading(btn, true);
309
+ try {
310
+ const data = await apiFetch("/api/history");
311
+ const tbody = $("history-body");
312
+ if (!data.entries?.length) {
313
+ tbody.innerHTML = `<tr><td class="empty-msg" colspan="6">No prompts yet. Generate your first one above!</td></tr>`;
314
+ return;
315
+ }
316
+ tbody.innerHTML = data.entries.map(e => `
317
+ <tr>
318
+ <td><code style="font-size:.75rem;color:var(--text-muted)">${esc(e.prompt_id?.slice(0,8))}…</code></td>
319
+ <td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.instruction?.slice(0,60) || "—")}</td>
320
+ <td>v${e.version || 1}</td>
321
+ <td><span class="badge badge-${e.status || 'pending'}">${esc(e.status || "pending")}</span></td>
322
+ <td style="white-space:nowrap">${e.created_at ? new Date(e.created_at).toLocaleDateString() : "—"}</td>
323
+ <td>
324
+ <button class="btn-secondary btn-sm btn-danger" onclick="deletePrompt('${esc(e.prompt_id)}')">🗑 Delete</button>
325
+ </td>
326
+ </tr>`).join("");
327
+ toast(`Loaded ${data.total} prompt(s).`, "info");
328
+ } catch (e) {
329
+ toast(`Failed to load history: ${e.message}`, "error");
330
+ } finally {
331
+ setLoading(btn, false);
332
+ }
333
+ }
334
+
335
+ async function deletePrompt(id) {
336
+ if (!confirm("Delete this prompt?")) return;
337
+ try {
338
+ await apiFetch(`/api/history/${id}`, "DELETE");
339
+ toast("Prompt deleted.", "success");
340
+ loadHistory();
341
+ } catch (e) {
342
+ toast(`Delete failed: ${e.message}`, "error");
343
+ }
344
+ }
frontend/index.html CHANGED
@@ -3,17 +3,15 @@
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>PromptForge — Neural Prompt Generator</title>
7
  <link rel="stylesheet" href="/static/style.css"/>
8
  <meta name="description" content="Transform raw instructions into structured, ready-to-use prompts for Google AI Studio."/>
9
  </head>
10
  <body>
11
 
12
- <!-- ── Custom Cursor ─────────────────────────────────────────────── -->
13
  <div id="cursor-dot"></div>
14
  <div id="cursor-ring"></div>
15
-
16
- <!-- ── Background Layers ─────────────────────────────────────────── -->
17
  <div class="bg-mesh"></div>
18
  <div class="bg-grid"></div>
19
  <div class="orb orb-1"></div>
@@ -22,27 +20,64 @@
22
 
23
  <div id="app">
24
 
25
- <!-- ── Header ─────────────────────────────────────────────────── -->
26
  <header>
27
  <div class="header-inner">
28
  <div class="logo-group">
29
- <div class="logo-icon">⚙️</div>
30
  <span class="logo-text">PromptForge</span>
31
- <span class="logo-tag">v2.0</span>
32
  </div>
33
  <div class="header-meta">
34
  <div class="status-pill">
35
  <div class="status-dot"></div>
36
  <span>ONLINE</span>
37
  </div>
38
- <a class="nav-link" href="/docs" target="_blank"> API Docs</a>
39
  </div>
40
  </div>
41
  </header>
42
 
43
  <main>
44
 
45
- <!-- ── Step Progress ─────────────────────────────────────────── -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  <div class="step-progress">
47
  <div class="prog-step active" id="pstep-1">
48
  <div class="prog-node">1</div>
@@ -70,16 +105,16 @@
70
  </div>
71
  </div>
72
 
73
- <!-- ── STEP 1: Input ─────────────────────────────────────────── -->
74
  <section id="step-input" class="card">
75
  <div class="card-header">
76
- <h2>Enter Your Instruction</h2>
77
  <span class="step-badge">STEP 01</span>
78
  </div>
79
 
80
  <div class="info-banner">
81
  <span class="info-icon">💡</span>
82
- <span>Describe any task in plain English. PromptForge will transform it into a fully structured prompt ready for Google AI Studio.</span>
83
  </div>
84
 
85
  <div class="field">
@@ -94,36 +129,19 @@
94
  placeholder="e.g. Support dark mode, must be accessible (WCAG AA), use React hooks only."></textarea>
95
  </div>
96
 
97
- <div class="input-grid">
98
- <div class="field">
99
- <label><span class="lbl-dot"></span> AI Enhancement</label>
100
- <select id="provider">
101
- <option value="none">⚡ Local Engine (no API)</option>
102
- <option value="google">🌐 Google Gemini</option>
103
- <option value="huggingface">🤗 Hugging Face</option>
104
- </select>
105
- <div class="field-note">Optionally enhance via AI after generation.</div>
106
- </div>
107
- <div class="field hidden" id="api-key-group">
108
- <label><span class="lbl-dot"></span> API Key <span class="lbl-opt">never stored</span></label>
109
- <input type="password" id="api-key" placeholder="Paste your API key here…"/>
110
- <div class="field-note">Transmitted securely, never logged.</div>
111
- </div>
112
- </div>
113
-
114
  <div class="action-row">
115
  <button id="btn-generate" class="btn-primary">⚡ Generate Prompt Manifest</button>
116
  </div>
117
  </section>
118
 
119
- <!-- ── STEP 2: Manifest Review ──────────────────────────────── -->
120
  <section id="step-manifest" class="card hidden fade-in">
121
  <div class="card-header">
122
- <h2>Review &amp; Edit Manifest</h2>
123
  <span class="step-badge">STEP 02</span>
124
  </div>
125
 
126
- <p class="muted">Every field is editable. Tweak anything before approving — the final prompt will regenerate automatically.</p>
127
 
128
  <div id="manifest-grid" class="manifest-grid"></div>
129
 
@@ -138,10 +156,10 @@
138
  </div>
139
  </section>
140
 
141
- <!-- ── STEP 3: Finalized Prompt ─────────────────────────────── -->
142
  <section id="step-finalized" class="card hidden fade-in">
143
  <div class="card-header">
144
- <h2>Finalized Prompt</h2>
145
  <span class="step-badge">STEP 03</span>
146
  </div>
147
 
@@ -171,10 +189,10 @@
171
  </div>
172
  </section>
173
 
174
- <!-- ── STEP 5: Refine ───────────────────────────────────────── -->
175
  <section id="step-refine" class="card hidden fade-in">
176
  <div class="card-header">
177
- <h2>Refine Prompt</h2>
178
  <span class="step-badge">STEP 05</span>
179
  </div>
180
  <p class="muted">Describe what to change. PromptForge will generate a new version (v+1) of the manifest for re-approval.</p>
@@ -191,10 +209,10 @@
191
  </div>
192
  </section>
193
 
194
- <!-- ── History ──────────────────────────────────────────────── -->
195
  <section id="section-history" class="card">
196
  <div class="card-header">
197
- <h2>Prompt History</h2>
198
  <button id="btn-load-history" class="btn-secondary btn-sm">↺ Refresh</button>
199
  </div>
200
 
@@ -219,13 +237,13 @@
219
 
220
  </main>
221
 
222
- <!-- ── Footer ─────────────────────────────────────────────────── -->
223
  <footer>
224
  <div class="footer-inner">
225
- <span class="footer-copy">© 2025 PromptForge — Neural Prompt Generator</span>
226
  <div class="footer-links">
227
- <a href="/docs" target="_blank">API Docs</a>
228
- <a href="/redoc" target="_blank">ReDoc</a>
229
  <a href="/health" target="_blank">Health</a>
230
  </div>
231
  </div>
@@ -233,9 +251,7 @@
233
 
234
  </div><!-- #app -->
235
 
236
- <!-- ── Toast container ─────────────────────────────────────────── -->
237
  <div id="toast-container"></div>
238
-
239
  <script src="/static/client.js"></script>
240
  </body>
241
  </html>
 
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>PromptForge — Structured Prompt Generator</title>
7
  <link rel="stylesheet" href="/static/style.css"/>
8
  <meta name="description" content="Transform raw instructions into structured, ready-to-use prompts for Google AI Studio."/>
9
  </head>
10
  <body>
11
 
12
+ <!-- These elements kept for JS compatibility but hidden via CSS -->
13
  <div id="cursor-dot"></div>
14
  <div id="cursor-ring"></div>
 
 
15
  <div class="bg-mesh"></div>
16
  <div class="bg-grid"></div>
17
  <div class="orb orb-1"></div>
 
20
 
21
  <div id="app">
22
 
23
+ <!-- ── Header ─────────────────────────────────────────────────── -->
24
  <header>
25
  <div class="header-inner">
26
  <div class="logo-group">
27
+ <div class="logo-icon"></div>
28
  <span class="logo-text">PromptForge</span>
29
+ <span class="logo-tag">v3.0</span>
30
  </div>
31
  <div class="header-meta">
32
  <div class="status-pill">
33
  <div class="status-dot"></div>
34
  <span>ONLINE</span>
35
  </div>
36
+ <a class="nav-link" href="/docs" target="_blank">📖 API Docs</a>
37
  </div>
38
  </div>
39
  </header>
40
 
41
  <main>
42
 
43
+ <!-- ── API Configuration Banner ─────────────────────────────── -->
44
+ <div class="api-config-banner">
45
+ <div class="api-banner-header">
46
+ <div class="api-banner-icon">🔑</div>
47
+ <div class="api-banner-title">
48
+ <h3>AI Enhancement Settings</h3>
49
+ <p>Enter your API key to unlock AI-powered prompt enhancement (optional — works without a key too)</p>
50
+ </div>
51
+ </div>
52
+ <div class="api-row">
53
+ <div class="api-field">
54
+ <label><span class="lbl-dot"></span> AI Provider</label>
55
+ <select id="provider">
56
+ <option value="none">⚡ Local Engine (no API key needed)</option>
57
+ <option value="google">🌐 Google Gemini</option>
58
+ <option value="huggingface">🤗 Hugging Face</option>
59
+ </select>
60
+ </div>
61
+ <div class="api-field" id="api-key-group">
62
+ <label>
63
+ <span class="lbl-dot"></span> API Key
64
+ <span class="lbl-opt" id="api-key-hint">Select a provider above</span>
65
+ </label>
66
+ <div class="api-input-wrap">
67
+ <input type="password" id="api-key"
68
+ placeholder="Paste your API key here…"
69
+ disabled/>
70
+ <button class="api-toggle-btn" id="btn-toggle-key" title="Show/hide key">👁</button>
71
+ </div>
72
+ <div class="api-field-note">
73
+ <span class="api-dot" id="api-status-dot"></span>
74
+ <span id="api-status-text">No key entered</span>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- ── Step Progress ──────────────────────────────────────────── -->
81
  <div class="step-progress">
82
  <div class="prog-step active" id="pstep-1">
83
  <div class="prog-node">1</div>
 
105
  </div>
106
  </div>
107
 
108
+ <!-- ── STEP 1: Input ─────────────────────────────────────────── -->
109
  <section id="step-input" class="card">
110
  <div class="card-header">
111
+ <h2>✍️ Enter Your Instruction</h2>
112
  <span class="step-badge">STEP 01</span>
113
  </div>
114
 
115
  <div class="info-banner">
116
  <span class="info-icon">💡</span>
117
+ <span>Describe any task in plain English. PromptForge will transform it into a fully structured, ready-to-use prompt for Google AI Studio.</span>
118
  </div>
119
 
120
  <div class="field">
 
129
  placeholder="e.g. Support dark mode, must be accessible (WCAG AA), use React hooks only."></textarea>
130
  </div>
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  <div class="action-row">
133
  <button id="btn-generate" class="btn-primary">⚡ Generate Prompt Manifest</button>
134
  </div>
135
  </section>
136
 
137
+ <!-- ── STEP 2: Manifest Review ──────────────────────────────── -->
138
  <section id="step-manifest" class="card hidden fade-in">
139
  <div class="card-header">
140
+ <h2>🔍 Review &amp; Edit Manifest</h2>
141
  <span class="step-badge">STEP 02</span>
142
  </div>
143
 
144
+ <p class="muted">Every field is editable. Tweak anything before approving — the final prompt will regenerate from your edits.</p>
145
 
146
  <div id="manifest-grid" class="manifest-grid"></div>
147
 
 
156
  </div>
157
  </section>
158
 
159
+ <!-- ── STEP 3: Finalized Prompt ─────────────────────────────── -->
160
  <section id="step-finalized" class="card hidden fade-in">
161
  <div class="card-header">
162
+ <h2>🎉 Finalized Prompt</h2>
163
  <span class="step-badge">STEP 03</span>
164
  </div>
165
 
 
189
  </div>
190
  </section>
191
 
192
+ <!-- ── STEP 5: Refine ───────────────────────────────────────── -->
193
  <section id="step-refine" class="card hidden fade-in">
194
  <div class="card-header">
195
+ <h2>🔁 Refine Prompt</h2>
196
  <span class="step-badge">STEP 05</span>
197
  </div>
198
  <p class="muted">Describe what to change. PromptForge will generate a new version (v+1) of the manifest for re-approval.</p>
 
209
  </div>
210
  </section>
211
 
212
+ <!-- ── History ──────────────────────────────────────────────── -->
213
  <section id="section-history" class="card">
214
  <div class="card-header">
215
+ <h2>📜 Prompt History</h2>
216
  <button id="btn-load-history" class="btn-secondary btn-sm">↺ Refresh</button>
217
  </div>
218
 
 
237
 
238
  </main>
239
 
240
+ <!-- ── Footer ─────────────────────────────────────────────────── -->
241
  <footer>
242
  <div class="footer-inner">
243
+ <span class="footer-copy">© 2025 PromptForge — Structured Prompt Generator · Port 7860</span>
244
  <div class="footer-links">
245
+ <a href="/docs" target="_blank">API Docs</a>
246
+ <a href="/redoc" target="_blank">ReDoc</a>
247
  <a href="/health" target="_blank">Health</a>
248
  </div>
249
  </div>
 
251
 
252
  </div><!-- #app -->
253
 
 
254
  <div id="toast-container"></div>
 
255
  <script src="/static/client.js"></script>
256
  </body>
257
  </html>
frontend/style.css CHANGED
@@ -1,199 +1,208 @@
1
  /* ═══════════════════════════════════════════════════════════════
2
- PROMPTFORGE — Neural Forge UI v2.0
3
- Aesthetic: Deep Space Cyberpunk + Glassmorphism
4
  ═══════════════════════════════════════════════════════════════ */
5
 
6
- @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;500;600;700;800&family=DM+Mono:ital,wght@0,300;0,400;0,500;1,400&family=Outfit:wght@300;400;500;600&display=swap');
7
 
8
  :root {
9
- --void:#020408; --deep:#060c18;
10
- --surface-1:rgba(8,15,32,.85); --surface-2:rgba(12,22,48,.75); --surface-3:rgba(16,28,58,.6);
11
- --cyan:#00f5d4; --cyan-dim:rgba(0,245,212,.12); --cyan-glow:rgba(0,245,212,.4);
12
- --magenta:#f72585; --magenta-dim:rgba(247,37,133,.12);
13
- --violet:#7209b7; --violet-dim:rgba(114,9,183,.18); --gold:#ffd60a;
14
- --text:#e8edf8; --text-soft:#a8b4cc; --text-muted:#5a6a8a; --text-dim:#3a4a6a;
15
- --border:rgba(0,245,212,.1); --border-hot:rgba(0,245,212,.32);
16
- --font-display:'Syne',sans-serif; --font-body:'Outfit',sans-serif; --font-mono:'DM Mono',monospace;
17
- --radius-sm:8px; --radius-md:14px; --radius-lg:20px;
18
- --shadow-card:0 0 0 1px rgba(0,245,212,.07),0 24px 64px rgba(0,0,0,.65),0 4px 16px rgba(0,0,0,.4);
19
- --shadow-hover:0 0 0 1px rgba(0,245,212,.22),0 32px 80px rgba(0,0,0,.7),0 0 40px rgba(0,245,212,.05);
20
- --glow-cyan:0 0 24px rgba(0,245,212,.55),0 0 60px rgba(0,245,212,.2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  }
 
22
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
23
- html{scroll-behavior:smooth;scrollbar-width:thin;scrollbar-color:var(--cyan-dim) transparent}
24
- ::-webkit-scrollbar{width:4px}::-webkit-scrollbar-thumb{background:var(--cyan-dim);border-radius:4px}
25
- body{background:var(--void);color:var(--text);font-family:var(--font-body);font-size:15px;line-height:1.65;min-height:100vh;overflow-x:hidden;cursor:none}
26
- @media(max-width:680px){body{cursor:auto}button{cursor:pointer!important}}
27
-
28
- /* Cursor */
29
- #cursor-dot{position:fixed;width:8px;height:8px;background:var(--cyan);border-radius:50%;pointer-events:none;z-index:9999;transform:translate(-50%,-50%);transition:width .2s,height .2s,background .2s;box-shadow:var(--glow-cyan)}
30
- #cursor-ring{position:fixed;width:36px;height:36px;border:1px solid rgba(0,245,212,.45);border-radius:50%;pointer-events:none;z-index:9998;transform:translate(-50%,-50%);transition:all .1s ease}
31
- @media(max-width:680px){#cursor-dot,#cursor-ring{display:none}}
32
-
33
- /* Backgrounds */
34
- .bg-mesh{position:fixed;inset:0;z-index:0;pointer-events:none;background:radial-gradient(ellipse 80% 50% at 15% 15%,rgba(0,245,212,.065) 0%,transparent 60%),radial-gradient(ellipse 60% 40% at 85% 85%,rgba(247,37,133,.05) 0%,transparent 60%),radial-gradient(ellipse 50% 60% at 60% 5%,rgba(114,9,183,.04) 0%,transparent 55%);animation:meshAnim 22s ease-in-out infinite alternate}
35
- @keyframes meshAnim{0%{filter:hue-rotate(0deg) brightness(1)}50%{filter:hue-rotate(18deg) brightness(1.08)}100%{filter:hue-rotate(-8deg) brightness(.96)}}
36
- .bg-grid{position:fixed;inset:0;z-index:0;pointer-events:none;background-image:linear-gradient(rgba(0,245,212,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(0,245,212,.025) 1px,transparent 1px);background-size:60px 60px;mask-image:radial-gradient(ellipse 100% 80% at 50% 0%,black 30%,transparent 75%)}
37
- .orb{position:fixed;border-radius:50%;filter:blur(110px);pointer-events:none;z-index:0;animation:orbFloat 28s ease-in-out infinite}
38
- .orb-1{width:700px;height:700px;background:radial-gradient(circle,rgba(0,245,212,.055) 0%,transparent 70%);top:-250px;left:-250px;animation-duration:32s}
39
- .orb-2{width:500px;height:500px;background:radial-gradient(circle,rgba(247,37,133,.045) 0%,transparent 70%);bottom:-150px;right:-150px;animation-duration:24s;animation-delay:-12s}
40
- .orb-3{width:400px;height:400px;background:radial-gradient(circle,rgba(114,9,183,.038) 0%,transparent 70%);top:45%;left:50%;animation-duration:36s;animation-delay:-20s}
41
- @keyframes orbFloat{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(70px,-50px) scale(1.12)}66%{transform:translate(-50px,70px) scale(.9)}}
42
-
43
- /* Layout */
44
- #app{position:relative;z-index:1;min-height:100vh;display:flex;flex-direction:column}
45
- main{max-width:900px;width:100%;margin:0 auto;padding:20px 20px 80px;display:flex;flex-direction:column;gap:20px;flex:1}
46
-
47
- /* Header */
48
- header{position:sticky;top:0;z-index:100;backdrop-filter:blur(28px) saturate(180%);-webkit-backdrop-filter:blur(28px) saturate(180%);background:rgba(2,4,8,.88);border-bottom:1px solid rgba(0,245,212,.07)}
49
- .header-inner{max-width:900px;margin:0 auto;padding:13px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px}
50
- .logo-group{display:flex;align-items:center;gap:12px}
51
- .logo-icon{width:40px;height:40px;flex-shrink:0;background:linear-gradient(135deg,var(--cyan),var(--violet));border-radius:11px;display:grid;place-items:center;font-size:18px;box-shadow:0 0 22px rgba(0,245,212,.28);animation:logoPulse 4s ease-in-out infinite}
52
- @keyframes logoPulse{0%,100%{box-shadow:0 0 22px rgba(0,245,212,.28)}50%{box-shadow:0 0 40px rgba(0,245,212,.5),0 0 80px rgba(0,245,212,.1)}}
53
- .logo-text{font-family:var(--font-display);font-size:1.25rem;font-weight:800;letter-spacing:-.3px;background:linear-gradient(90deg,var(--cyan),#b8f0e6);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
54
- .logo-tag{font-family:var(--font-mono);font-size:.6rem;color:var(--cyan);background:var(--cyan-dim);border:1px solid rgba(0,245,212,.18);padding:2px 8px;border-radius:20px;letter-spacing:1px}
55
- .header-meta{display:flex;align-items:center;gap:14px}
56
- .status-pill{display:flex;align-items:center;gap:7px;font-family:var(--font-mono);font-size:.72rem;color:var(--text-muted);background:rgba(0,245,212,.04);border:1px solid var(--border);padding:5px 12px;border-radius:20px}
57
- .status-dot{width:6px;height:6px;border-radius:50%;background:var(--cyan);box-shadow:0 0 8px var(--cyan);animation:blink 2.2s ease-in-out infinite}
58
- @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
59
- .nav-link{font-family:var(--font-mono);font-size:.72rem;color:var(--text-muted);text-decoration:none;display:flex;align-items:center;gap:5px;padding:5px 12px;border:1px solid var(--border);border-radius:6px;transition:all .2s}
60
- .nav-link:hover{color:var(--cyan);border-color:var(--border-hot);background:var(--cyan-dim)}
61
-
62
- /* Step Progress */
63
- .step-progress{display:flex;align-items:center;justify-content:center;padding:18px 0 2px;gap:0}
 
 
 
 
 
 
 
64
  .prog-step{display:flex;flex-direction:column;align-items:center;gap:5px}
65
- .prog-node{width:30px;height:30px;border-radius:50%;border:1px solid var(--border);background:var(--surface-1);display:grid;place-items:center;font-family:var(--font-mono);font-size:.7rem;color:var(--text-muted);transition:all .4s cubic-bezier(.34,1.56,.64,1);position:relative;z-index:1}
66
- .prog-step.active .prog-node{border-color:var(--cyan);background:var(--cyan-dim);color:var(--cyan);box-shadow:0 0 18px rgba(0,245,212,.4);transform:scale(1.18)}
67
- .prog-step.done .prog-node{border-color:var(--cyan);background:var(--cyan);color:var(--void);font-weight:700}
68
- .prog-label{font-size:.6rem;color:var(--text-dim);font-family:var(--font-mono);letter-spacing:.5px;text-transform:uppercase}
69
- .prog-step.active .prog-label{color:var(--cyan)}.prog-step.done .prog-label{color:var(--text-muted)}
70
- .prog-line{width:55px;height:1px;background:var(--border);margin-bottom:20px;position:relative;overflow:hidden}
71
- .prog-line::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,var(--cyan),var(--magenta));transform:scaleX(0);transform-origin:left;transition:transform .6s ease}
72
- .prog-line.filled::after{transform:scaleX(1)}
73
-
74
- /* Cards */
75
- .card{backdrop-filter:blur(20px) saturate(160%);-webkit-backdrop-filter:blur(20px) saturate(160%);background:var(--surface-1);border:1px solid var(--border);border-radius:var(--radius-lg);padding:30px 34px;box-shadow:var(--shadow-card);position:relative;overflow:hidden;transition:box-shadow .4s,border-color .4s,transform .3s;animation:cardIn .5s cubic-bezier(.22,1,.36,1) both}
76
- @keyframes cardIn{from{opacity:0;transform:translateY(18px)}to{opacity:1;transform:translateY(0)}}
77
- .card:hover{box-shadow:var(--shadow-hover);border-color:rgba(0,245,212,.16)}
78
- .card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(0,245,212,.5),transparent);opacity:.6}
79
- .card::after{content:'';position:absolute;top:-50px;right:-50px;width:130px;height:130px;border-radius:50%;background:radial-gradient(circle,rgba(0,245,212,.035) 0%,transparent 70%);pointer-events:none}
80
- .card-header{display:flex;align-items:center;gap:12px;margin-bottom:26px}
81
- .card-header h2{font-family:var(--font-display);font-size:1rem;font-weight:700;color:var(--text);letter-spacing:-.2px;flex:1}
82
- .step-badge{font-family:var(--font-mono);font-size:.58rem;font-weight:500;letter-spacing:1.5px;text-transform:uppercase;color:var(--void);background:linear-gradient(90deg,var(--cyan),#00c4a8);padding:3px 10px;border-radius:20px;box-shadow:0 0 12px rgba(0,245,212,.38)}
83
-
84
- /* Forms */
85
- .field{margin-bottom:20px}
86
- label{display:flex;align-items:center;gap:7px;font-family:var(--font-mono);font-size:.7rem;font-weight:500;letter-spacing:1px;text-transform:uppercase;color:var(--text-muted);margin-bottom:7px}
87
- .lbl-dot{width:4px;height:4px;border-radius:50%;background:var(--cyan);box-shadow:0 0 6px var(--cyan)}
88
- .lbl-opt{color:var(--text-dim);font-size:.62rem;margin-left:auto;letter-spacing:.5px;text-transform:none}
89
- textarea,input[type="password"],select{width:100%;background:rgba(4,9,20,.85);border:1px solid rgba(0,245,212,.1);border-radius:var(--radius-md);color:var(--text);font-family:var(--font-body);font-size:.9rem;padding:12px 16px;resize:vertical;outline:none;transition:border-color .3s,box-shadow .3s,background .3s;box-shadow:inset 0 1px 0 rgba(255,255,255,.02),0 1px 4px rgba(0,0,0,.35)}
90
- textarea:focus,input:focus,select:focus{border-color:var(--cyan);background:rgba(0,245,212,.025);box-shadow:0 0 0 3px rgba(0,245,212,.07),0 0 20px rgba(0,245,212,.08)}
91
- textarea::placeholder,input::placeholder{color:var(--text-dim);font-style:italic}
92
- textarea{min-height:110px;line-height:1.7}
93
- select{appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='11' height='7' viewBox='0 0 11 7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5.5 6L10 1' stroke='%2300f5d4' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:38px}
94
- select option{background:#06101e}
95
- .input-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
96
- .field-note{font-size:.72rem;color:var(--text-dim);margin-top:5px;font-family:var(--font-mono)}
97
-
98
- /* Buttons */
99
- button{cursor:none}
100
- .btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;border-radius:var(--radius-md);font-family:var(--font-display);font-size:.9rem;font-weight:700;letter-spacing:.3px;padding:13px 28px;position:relative;overflow:hidden;background:linear-gradient(135deg,var(--cyan) 0%,#00d4b4 40%,#00a88e 70%,var(--violet) 100%);background-size:250% 250%;color:var(--void);animation:gradFlow 5s ease infinite;transition:transform .3s cubic-bezier(.34,1.56,.64,1),box-shadow .3s}
101
- @keyframes gradFlow{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
102
- .btn-primary::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.2),transparent);opacity:0;transition:opacity .3s}
103
- .btn-primary::after{content:'';position:absolute;top:0;left:-100%;width:55%;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.22),transparent);transform:skewX(-20deg)}
104
- .btn-primary:hover{transform:translateY(-2px) scale(1.02);box-shadow:0 10px 32px var(--cyan-glow),0 0 60px rgba(0,245,212,.12)}
105
- .btn-primary:hover::before{opacity:1}
106
- .btn-primary:hover::after{animation:shimmer .65s ease forwards}
107
- @keyframes shimmer{to{left:150%}}
108
- .btn-primary:active{transform:translateY(0) scale(.99)}
109
- .btn-primary:disabled{opacity:.6;transform:none;cursor:not-allowed;animation:none}
110
- .btn-secondary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:1px solid var(--border);border-radius:var(--radius-md);background:var(--surface-2);color:var(--text-soft);font-family:var(--font-body);font-size:.875rem;font-weight:500;padding:11px 22px;transition:all .25s;backdrop-filter:blur(8px);position:relative;overflow:hidden}
111
- .btn-secondary::before{content:'';position:absolute;inset:0;background:linear-gradient(135deg,var(--cyan-dim),transparent);opacity:0;transition:opacity .3s}
112
- .btn-secondary:hover{border-color:var(--border-hot);color:var(--cyan);transform:translateY(-1px);box-shadow:0 4px 18px rgba(0,245,212,.1)}
113
- .btn-secondary:hover::before{opacity:1}
114
- .btn-danger:hover{border-color:var(--magenta);color:var(--magenta);background:var(--magenta-dim);box-shadow:0 4px 18px rgba(247,37,133,.12)}
115
- .btn-sm{font-size:.76rem;padding:6px 13px;border-radius:var(--radius-sm)}
116
- .action-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:22px;align-items:center}
117
- .btn-copy{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:8px;background:var(--surface-3);color:var(--text-muted);font-family:var(--font-mono);font-size:.73rem;padding:7px 14px;margin-top:10px;transition:all .25s}
118
- .btn-copy:hover{color:var(--cyan);border-color:var(--border-hot);background:var(--cyan-dim)}
119
- .btn-copy.copied{color:var(--cyan);border-color:var(--cyan);background:var(--cyan-dim)}
120
-
121
- /* Manifest */
122
- .manifest-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:22px}
123
- .manifest-field{display:flex;flex-direction:column;gap:5px}
124
  .manifest-field.full{grid-column:1/-1}
125
- .manifest-field label{font-size:.65rem;letter-spacing:1.5px;color:var(--cyan)}
126
- .manifest-field textarea,.manifest-field input{font-size:.84rem;min-height:62px;border-radius:var(--radius-sm);padding:10px 13px}
127
-
128
- /* Details */
129
- details{border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;transition:border-color .3s}
130
- details[open]{border-color:rgba(0,245,212,.18)}
131
- details summary{padding:11px 16px;cursor:none;font-family:var(--font-mono);font-size:.78rem;color:var(--text-muted);background:var(--surface-2);display:flex;align-items:center;gap:8px;list-style:none;user-select:none;transition:all .2s}
132
- details summary::-webkit-details-marker{display:none}
133
- details summary::before{content:'▶';font-size:.5rem;color:var(--cyan);transition:transform .3s}
 
 
 
 
 
 
134
  details[open] summary::before{transform:rotate(90deg)}
135
- details summary:hover{color:var(--text);background:var(--surface-3)}
136
-
137
- /* Pre */
138
- pre{background:rgba(2,5,12,.92);border:1px solid rgba(0,245,212,.06);border-radius:var(--radius-md);color:#a0d0c4;font-family:var(--font-mono);font-size:.79rem;line-height:1.6;overflow:auto;padding:20px;white-space:pre-wrap;word-break:break-word;max-height:420px;position:relative}
139
- pre::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(0,245,212,.25),transparent)}
140
-
141
- /* Tabs */
142
- .tab-bar{display:flex;gap:4px;margin-bottom:16px;background:rgba(4,9,20,.7);padding:4px;border-radius:var(--radius-md);border:1px solid var(--border);width:fit-content}
143
- .tab{border:none;border-radius:9px;background:none;color:var(--text-muted);font-family:var(--font-mono);font-size:.76rem;letter-spacing:.5px;padding:7px 18px;transition:all .25s}
144
- .tab.active{background:linear-gradient(135deg,rgba(0,245,212,.18),rgba(0,245,212,.06));color:var(--cyan);border:1px solid rgba(0,245,212,.22);box-shadow:0 0 14px rgba(0,245,212,.14)}
145
- .tab:not(.active):hover{color:var(--text-soft)}
146
-
147
- /* Table */
148
- .table-wrap{margin-top:14px;border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden}
149
- table{width:100%;border-collapse:collapse;font-size:.82rem}
150
- thead{background:rgba(0,245,212,.03);border-bottom:1px solid var(--border)}
151
- th{padding:11px 15px;text-align:left;font-family:var(--font-mono);font-size:.64rem;letter-spacing:1px;text-transform:uppercase;color:var(--text-muted);font-weight:400}
152
- td{padding:11px 15px;border-bottom:1px solid rgba(0,245,212,.03);vertical-align:middle;color:var(--text-soft);transition:background .2s}
153
  tr:last-child td{border-bottom:none}
154
- tbody tr:hover td{background:rgba(0,245,212,.025)}
155
- td code{font-family:var(--font-mono);font-size:.76rem;color:var(--cyan);background:var(--cyan-dim);padding:2px 8px;border-radius:4px}
156
- td.empty-msg{padding:36px;text-align:center;color:var(--text-dim);font-style:italic}
157
-
158
- /* Badges */
159
- .badge{display:inline-flex;align-items:center;gap:5px;border-radius:20px;font-family:var(--font-mono);font-size:.64rem;letter-spacing:.5px;text-transform:uppercase;padding:3px 10px}
160
- .badge::before{content:'';width:5px;height:5px;border-radius:50%;flex-shrink:0}
161
- .badge-pending{background:rgba(255,214,10,.1);color:var(--gold);border:1px solid rgba(255,214,10,.2)}
162
- .badge-pending::before{background:var(--gold);box-shadow:0 0 5px var(--gold)}
163
- .badge-approved{background:rgba(0,245,212,.1);color:var(--cyan);border:1px solid rgba(0,245,212,.2)}
164
- .badge-approved::before{background:var(--cyan);box-shadow:0 0 5px var(--cyan)}
165
- .badge-exported{background:rgba(247,37,133,.1);color:var(--magenta);border:1px solid rgba(247,37,133,.2)}
166
- .badge-exported::before{background:var(--magenta);box-shadow:0 0 5px var(--magenta)}
167
-
168
- /* Info banner */
169
- .info-banner{display:flex;align-items:flex-start;gap:11px;background:rgba(0,245,212,.04);border:1px solid rgba(0,245,212,.1);border-radius:var(--radius-md);padding:13px 16px;font-size:.83rem;color:var(--text-soft);margin-bottom:20px}
170
- .info-icon{font-size:15px;flex-shrink:0;margin-top:1px}
171
- .divider{height:1px;background:linear-gradient(90deg,transparent,var(--border),transparent);margin:22px 0}
172
-
173
- /* Toasts */
174
- #toast-container{position:fixed;bottom:26px;right:26px;z-index:9990;display:flex;flex-direction:column;gap:9px;pointer-events:none}
175
- .toast{pointer-events:all;backdrop-filter:blur(22px);background:rgba(6,12,24,.96);border:1px solid var(--border);border-radius:var(--radius-md);box-shadow:0 8px 36px rgba(0,0,0,.55);display:flex;align-items:center;gap:11px;font-size:.84rem;max-width:360px;padding:13px 16px;animation:toastIn .35s cubic-bezier(.22,1,.36,1) both}
176
  .toast.leaving{animation:toastOut .3s ease forwards}
177
- .toast-icon{width:28px;height:28px;border-radius:8px;display:grid;place-items:center;font-size:13px;flex-shrink:0}
178
- .toast.success{border-color:rgba(0,245,212,.22)}.toast.success .toast-icon{background:var(--cyan-dim)}
179
- .toast.error{border-color:rgba(247,37,133,.22)}.toast.error .toast-icon{background:var(--magenta-dim)}
180
- @keyframes toastIn{from{opacity:0;transform:translateX(16px) scale(.96)}to{opacity:1;transform:none}}
181
- @keyframes toastOut{to{opacity:0;transform:translateX(16px) scale(.96);max-height:0;padding:0;overflow:hidden}}
182
-
183
- /* Footer */
184
- footer{position:relative;z-index:1;border-top:1px solid rgba(0,245,212,.05);padding:22px 20px}
185
- .footer-inner{max-width:900px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px}
186
- .footer-copy{font-family:var(--font-mono);font-size:.7rem;color:var(--text-dim)}
 
187
  .footer-links{display:flex;gap:16px}
188
- .footer-links a{font-family:var(--font-mono);font-size:.7rem;color:var(--text-muted);text-decoration:none;transition:color .2s}
189
- .footer-links a:hover{color:var(--cyan)}
190
 
191
- /* Utility */
192
  .hidden{display:none!important}
193
- .muted{color:var(--text-muted);font-size:.84rem;margin-bottom:14px;line-height:1.55}
194
- .fade-in{animation:fadeIn .45s cubic-bezier(.22,1,.36,1) both}
195
- @keyframes fadeIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
196
-
197
- /* Responsive */
198
- @media(max-width:680px){.input-grid,.manifest-grid{grid-template-columns:1fr}.manifest-field.full{grid-column:1}.card{padding:22px 18px}main{padding:14px 14px 60px}.prog-line{width:28px}.footer-inner{flex-direction:column;align-items:center}}
199
- @media(max-width:420px){.header-meta{display:none}}
 
1
  /* ═══════════════════════════════════════════════════════════════
2
+ PROMPTFORGE — Clean Modern UI v3.0
3
+ Aesthetic: Professional SaaS · Light & Airy · Clear Hierarchy
4
  ═══════════════════════════════════════════════════════════════ */
5
 
6
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
7
 
8
  :root {
9
+ --bg: #f6f8fc;
10
+ --surface: #ffffff;
11
+ --surface-2: #f0f3f9;
12
+ --surface-3: #e8ecf5;
13
+ --border: #dde3ee;
14
+ --border-focus:#6366f1;
15
+ --indigo: #6366f1;
16
+ --indigo-dk: #4f46e5;
17
+ --indigo-lt: #eef0ff;
18
+ --indigo-mid: #c7d2fe;
19
+ --violet: #8b5cf6;
20
+ --green: #10b981;
21
+ --green-lt: #d1fae5;
22
+ --amber: #f59e0b;
23
+ --amber-lt: #fef3c7;
24
+ --red: #ef4444;
25
+ --red-lt: #fee2e2;
26
+ --text: #111827;
27
+ --text-soft: #374151;
28
+ --text-muted: #6b7280;
29
+ --text-faint: #9ca3af;
30
+ --radius-sm: 8px;
31
+ --radius-md: 12px;
32
+ --radius-lg: 16px;
33
+ --radius-xl: 20px;
34
+ --shadow-sm: 0 1px 3px rgba(0,0,0,.08),0 1px 2px rgba(0,0,0,.04);
35
+ --shadow-md: 0 4px 16px rgba(0,0,0,.08),0 1px 4px rgba(0,0,0,.05);
36
+ --shadow-lg: 0 12px 40px rgba(0,0,0,.10),0 4px 12px rgba(0,0,0,.06);
37
+ --shadow-btn: 0 2px 8px rgba(99,102,241,.30);
38
+ --font-body: 'Inter',system-ui,sans-serif;
39
+ --font-mono: 'JetBrains Mono','Fira Code',monospace;
40
  }
41
+
42
  *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
43
+ html{scroll-behavior:smooth}
44
+ body{background:var(--bg);color:var(--text);font-family:var(--font-body);font-size:15px;line-height:1.6;min-height:100vh;-webkit-font-smoothing:antialiased}
45
+ ::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}
46
+ ::-webkit-scrollbar-thumb{background:var(--border);border-radius:6px}
47
+ ::-webkit-scrollbar-thumb:hover{background:var(--indigo-mid)}
48
+
49
+ /* Remove dark elements */
50
+ #cursor-dot,#cursor-ring,.bg-mesh,.bg-grid,.orb{display:none!important}
51
+
52
+ /* ── Layout ── */
53
+ #app{min-height:100vh;display:flex;flex-direction:column}
54
+ main{max-width:860px;width:100%;margin:0 auto;padding:24px 20px 80px;display:flex;flex-direction:column;gap:20px;flex:1}
55
+
56
+ /* ── Header ── */
57
+ header{position:sticky;top:0;z-index:100;background:rgba(255,255,255,.94);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);box-shadow:0 1px 0 rgba(0,0,0,.04)}
58
+ .header-inner{max-width:860px;margin:0 auto;padding:14px 20px;display:flex;align-items:center;justify-content:space-between;gap:16px}
59
+ .logo-group{display:flex;align-items:center;gap:10px}
60
+ .logo-icon{width:36px;height:36px;background:linear-gradient(135deg,var(--indigo),var(--violet));border-radius:10px;display:grid;place-items:center;font-size:17px;box-shadow:0 2px 8px rgba(99,102,241,.35)}
61
+ .logo-text{font-size:1.1rem;font-weight:700;color:var(--text);letter-spacing:-.3px}
62
+ .logo-tag{font-family:var(--font-mono);font-size:.65rem;color:var(--indigo);background:var(--indigo-lt);border:1px solid var(--indigo-mid);padding:2px 8px;border-radius:20px;letter-spacing:.5px}
63
+ .header-meta{display:flex;align-items:center;gap:10px}
64
+ .status-pill{display:flex;align-items:center;gap:6px;font-size:.72rem;font-family:var(--font-mono);color:var(--text-muted);background:var(--surface-2);border:1px solid var(--border);padding:5px 12px;border-radius:20px}
65
+ .status-dot{width:6px;height:6px;border-radius:50%;background:var(--green);box-shadow:0 0 6px var(--green);animation:blink 2.5s ease-in-out infinite}
66
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:.35}}
67
+ .nav-link{font-size:.8rem;font-weight:500;color:var(--text-muted);text-decoration:none;padding:6px 14px;border:1px solid var(--border);border-radius:var(--radius-sm);transition:all .2s;background:var(--surface)}
68
+ .nav-link:hover{color:var(--indigo);border-color:var(--indigo-mid);background:var(--indigo-lt)}
69
+
70
+ /* ── API Config Banner ── */
71
+ .api-config-banner{background:var(--surface);border:2px solid var(--indigo-mid);border-radius:var(--radius-lg);padding:20px 24px;box-shadow:var(--shadow-sm)}
72
+ .api-banner-header{display:flex;align-items:center;gap:12px;margin-bottom:14px}
73
+ .api-banner-icon{width:40px;height:40px;flex-shrink:0;background:var(--indigo-lt);border-radius:var(--radius-md);display:grid;place-items:center;font-size:20px}
74
+ .api-banner-title h3{font-size:.95rem;font-weight:700;color:var(--text)}
75
+ .api-banner-title p{font-size:.8rem;color:var(--text-muted);margin-top:2px}
76
+ .api-row{display:grid;grid-template-columns:1fr 1fr;gap:14px}
77
+ @media(max-width:600px){.api-row{grid-template-columns:1fr}}
78
+ .api-field label{display:flex;align-items:center;gap:6px;font-size:.72rem;font-weight:600;color:var(--text-soft);letter-spacing:.4px;text-transform:uppercase;margin-bottom:7px}
79
+ .api-input-wrap{position:relative}
80
+ .api-input-wrap input{width:100%;padding:10px 40px 10px 14px;background:var(--surface-2);border:1.5px solid var(--border);border-radius:var(--radius-md);font-size:.85rem;font-family:var(--font-mono);color:var(--text);outline:none;transition:border-color .2s,box-shadow .2s,background .2s}
81
+ .api-input-wrap input:focus{border-color:var(--border-focus);background:white;box-shadow:0 0 0 3px rgba(99,102,241,.10)}
82
+ .api-input-wrap input::placeholder{color:var(--text-faint);font-style:italic;font-family:var(--font-body)}
83
+ .api-toggle-btn{position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:15px;padding:4px;transition:color .2s}
84
+ .api-toggle-btn:hover{color:var(--indigo)}
85
+ .api-field-note{font-size:.72rem;color:var(--text-muted);margin-top:5px;display:flex;align-items:center;gap:5px}
86
+ .api-dot{width:6px;height:6px;border-radius:50%;background:var(--text-faint);flex-shrink:0;transition:background .3s}
87
+ .api-dot.set{background:var(--green);box-shadow:0 0 6px var(--green)}
88
+
89
+ /* ── Step Progress ── */
90
+ .step-progress{display:flex;align-items:center;justify-content:center;padding:8px 0 4px;gap:0}
91
  .prog-step{display:flex;flex-direction:column;align-items:center;gap:5px}
92
+ .prog-node{width:32px;height:32px;border-radius:50%;border:2px solid var(--border);background:var(--surface);display:grid;place-items:center;font-size:.72rem;font-weight:600;color:var(--text-muted);transition:all .35s cubic-bezier(.34,1.56,.64,1)}
93
+ .prog-step.active .prog-node{border-color:var(--indigo);background:var(--indigo);color:white;box-shadow:0 0 0 4px rgba(99,102,241,.18);transform:scale(1.1)}
94
+ .prog-step.done .prog-node{border-color:var(--green);background:var(--green);color:white}
95
+ .prog-label{font-size:.6rem;color:var(--text-faint);font-weight:500;letter-spacing:.4px;text-transform:uppercase}
96
+ .prog-step.active .prog-label{color:var(--indigo);font-weight:600}
97
+ .prog-step.done .prog-label{color:var(--green)}
98
+ .prog-line{width:60px;height:2px;background:var(--border);margin-bottom:22px;border-radius:2px;overflow:hidden}
99
+ .prog-line::after{content:'';display:block;height:100%;width:0%;background:linear-gradient(90deg,var(--indigo),var(--green));transition:width .5s ease;border-radius:2px}
100
+ .prog-line.filled::after{width:100%}
101
+
102
+ /* ── Cards ── */
103
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius-xl);padding:28px 30px;box-shadow:var(--shadow-md);animation:cardIn .4s ease both}
104
+ @keyframes cardIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:translateY(0)}}
105
+ .card-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:22px;gap:12px}
106
+ .card-header h2{font-size:1.05rem;font-weight:700;color:var(--text);letter-spacing:-.2px}
107
+ .step-badge{font-family:var(--font-mono);font-size:.6rem;font-weight:600;letter-spacing:1.2px;text-transform:uppercase;color:var(--indigo);background:var(--indigo-lt);border:1px solid var(--indigo-mid);padding:3px 10px;border-radius:20px}
108
+
109
+ /* ── Info Banner ── */
110
+ .info-banner{display:flex;gap:10px;align-items:flex-start;background:var(--indigo-lt);border:1px solid var(--indigo-mid);border-radius:var(--radius-md);padding:12px 16px;margin-bottom:20px;font-size:.85rem;color:var(--indigo-dk);line-height:1.5}
111
+ .info-icon{font-size:1rem;flex-shrink:0;margin-top:1px}
112
+
113
+ /* ── Form ── */
114
+ .field{margin-bottom:18px}
115
+ label{display:flex;align-items:center;gap:6px;font-size:.75rem;font-weight:600;letter-spacing:.4px;text-transform:uppercase;color:var(--text-soft);margin-bottom:7px}
116
+ .lbl-dot{width:4px;height:4px;border-radius:50%;background:var(--indigo);flex-shrink:0}
117
+ .lbl-opt{color:var(--text-faint);font-size:.65rem;margin-left:auto;text-transform:none;letter-spacing:0;font-weight:400}
118
+ textarea,input[type="password"],select{width:100%;background:var(--surface-2);border:1.5px solid var(--border);border-radius:var(--radius-md);color:var(--text);font-family:var(--font-body);font-size:.9rem;padding:11px 14px;outline:none;transition:border-color .2s,box-shadow .2s,background .2s;resize:vertical}
119
+ textarea:focus,input:focus,select:focus{border-color:var(--border-focus);background:white;box-shadow:0 0 0 3px rgba(99,102,241,.10)}
120
+ textarea::placeholder,input::placeholder{color:var(--text-faint);font-style:italic}
121
+ textarea{min-height:100px;line-height:1.65}
122
+ select{appearance:none;cursor:pointer;background-image:url("data:image/svg+xml,%3Csvg width='11' height='7' viewBox='0 0 11 7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5.5 6L10 1' stroke='%236366f1' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;padding-right:38px}
123
+ .input-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
124
+ @media(max-width:600px){.input-grid{grid-template-columns:1fr}}
125
+ .field-note{font-size:.73rem;color:var(--text-muted);margin-top:5px}
126
+
127
+ /* ── Buttons ── */
128
+ button{cursor:pointer}
129
+ .btn-primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;border-radius:var(--radius-md);font-family:var(--font-body);font-size:.9rem;font-weight:600;padding:12px 26px;background:linear-gradient(135deg,var(--indigo) 0%,var(--violet) 100%);color:white;cursor:pointer;box-shadow:var(--shadow-btn);transition:transform .2s,box-shadow .2s,opacity .2s;position:relative;overflow:hidden}
130
+ .btn-primary:hover{transform:translateY(-1px);box-shadow:0 6px 20px rgba(99,102,241,.40)}
131
+ .btn-primary:active{transform:translateY(0)}
132
+ .btn-primary:disabled{opacity:.55;cursor:not-allowed;transform:none}
133
+ .btn-secondary{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1.5px solid var(--border);border-radius:var(--radius-md);background:var(--surface);color:var(--text-soft);font-family:var(--font-body);font-size:.875rem;font-weight:500;padding:10px 20px;cursor:pointer;transition:all .2s}
134
+ .btn-secondary:hover{border-color:var(--indigo-mid);color:var(--indigo);background:var(--indigo-lt)}
135
+ .btn-secondary:active{transform:scale(.98)}
136
+ .btn-danger{color:var(--red)}
137
+ .btn-danger:hover{border-color:var(--red);color:var(--red);background:var(--red-lt)}
138
+ .btn-sm{font-size:.78rem;padding:6px 14px;border-radius:var(--radius-sm)}
139
+ .action-row{display:flex;flex-wrap:wrap;gap:10px;margin-top:20px;align-items:center}
140
+ .spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:white;border-radius:50%;animation:spin .7s linear infinite;vertical-align:middle}
141
+ @keyframes spin{to{transform:rotate(360deg)}}
142
+
143
+ /* ── Manifest Grid ── */
144
+ .manifest-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:20px}
145
+ .manifest-field label{text-transform:uppercase;font-size:.68rem;color:var(--text-muted)}
146
+ .manifest-field textarea{min-height:70px;font-size:.85rem}
 
 
 
 
147
  .manifest-field.full{grid-column:1/-1}
148
+ @media(max-width:600px){.manifest-grid{grid-template-columns:1fr}.manifest-field.full{grid-column:auto}}
149
+
150
+ /* ── Tabs ── */
151
+ .tab-bar{display:flex;gap:4px;background:var(--surface-2);border:1px solid var(--border);border-radius:var(--radius-md);padding:4px;width:fit-content;margin-bottom:16px}
152
+ .tab{border:none;background:transparent;border-radius:var(--radius-sm);padding:7px 18px;font-size:.82rem;font-weight:500;color:var(--text-muted);cursor:pointer;transition:all .2s}
153
+ .tab.active{background:white;color:var(--indigo);box-shadow:var(--shadow-sm);font-weight:600}
154
+ .tab-panel{display:block}
155
+ .tab-panel.hidden{display:none}
156
+
157
+ /* ── Code ── */
158
+ pre{background:#1e1e2e;border-radius:var(--radius-md);padding:16px 18px;overflow-x:auto;font-family:var(--font-mono);font-size:.82rem;line-height:1.7;color:#cdd6f4;white-space:pre-wrap;word-break:break-word}
159
+ details{margin-top:12px}
160
+ summary{font-size:.83rem;font-weight:600;color:var(--text-muted);cursor:pointer;padding:10px 0;user-select:none;display:flex;align-items:center;gap:7px;list-style:none}
161
+ summary:hover{color:var(--indigo)}
162
+ summary::before{content:'▶';font-size:.7rem;transition:transform .2s}
163
  details[open] summary::before{transform:rotate(90deg)}
164
+ .btn-copy{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:var(--radius-sm);background:var(--surface);color:var(--text-muted);font-family:var(--font-mono);font-size:.73rem;padding:7px 14px;margin-top:10px;cursor:pointer;transition:all .2s}
165
+ .btn-copy:hover{color:var(--indigo);border-color:var(--indigo-mid);background:var(--indigo-lt)}
166
+ .btn-copy.copied{color:var(--green);border-color:var(--green);background:var(--green-lt)}
167
+
168
+ /* ── Misc ── */
169
+ .muted{font-size:.87rem;color:var(--text-muted);margin-bottom:18px;line-height:1.6}
170
+ .divider{height:1px;background:var(--border);margin:20px 0}
171
+
172
+ /* ── History Table ── */
173
+ .table-wrap{overflow-x:auto;border-radius:var(--radius-md);border:1px solid var(--border)}
174
+ table{width:100%;border-collapse:collapse;font-size:.85rem}
175
+ thead{background:var(--surface-2)}
176
+ th{padding:11px 14px;text-align:left;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.4px;color:var(--text-muted);border-bottom:1px solid var(--border);white-space:nowrap}
177
+ td{padding:11px 14px;color:var(--text-soft);border-bottom:1px solid var(--surface-2)}
 
 
 
 
178
  tr:last-child td{border-bottom:none}
179
+ tr:hover td{background:var(--surface-2)}
180
+ .empty-msg{color:var(--text-faint);font-style:italic;text-align:center;padding:28px 14px!important}
181
+ .badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:.7rem;font-weight:600;text-transform:uppercase;letter-spacing:.3px}
182
+ .badge-pending{background:var(--amber-lt);color:#92400e}
183
+ .badge-approved{background:var(--green-lt);color:#065f46}
184
+ .badge-exported{background:var(--indigo-lt);color:var(--indigo-dk)}
185
+
186
+ /* ── Toast ── */
187
+ #toast-container{position:fixed;bottom:24px;right:24px;display:flex;flex-direction:column;gap:10px;z-index:9999;pointer-events:none}
188
+ .toast{display:flex;align-items:center;gap:10px;background:white;border:1px solid var(--border);border-radius:var(--radius-md);padding:12px 18px;box-shadow:var(--shadow-lg);font-size:.85rem;color:var(--text);pointer-events:all;animation:toastIn .3s cubic-bezier(.34,1.56,.64,1) both;min-width:260px;max-width:380px}
 
 
 
 
 
 
 
 
 
 
 
 
189
  .toast.leaving{animation:toastOut .3s ease forwards}
190
+ @keyframes toastIn{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}
191
+ @keyframes toastOut{from{opacity:1;transform:translateX(0)}to{opacity:0;transform:translateX(20px)}}
192
+ .toast.success{border-left:4px solid var(--green)}
193
+ .toast.error{border-left:4px solid var(--red)}
194
+ .toast.warn{border-left:4px solid var(--amber)}
195
+ .toast.info{border-left:4px solid var(--indigo)}
196
+ .toast-icon{font-size:1.1rem;flex-shrink:0}
197
+
198
+ /* ── Footer ── */
199
+ footer{border-top:1px solid var(--border);background:var(--surface);padding:14px 20px}
200
+ .footer-inner{max-width:860px;margin:0 auto;display:flex;align-items:center;justify-content:space-between;gap:16px;font-size:.78rem;color:var(--text-faint)}
201
  .footer-links{display:flex;gap:16px}
202
+ .footer-links a{color:var(--text-muted);text-decoration:none;transition:color .2s}
203
+ .footer-links a:hover{color:var(--indigo)}
204
 
205
+ /* ── Utils ── */
206
  .hidden{display:none!important}
207
+ .fade-in{animation:cardIn .4s ease both}
208
+ @media(max-width:600px){main{padding:16px 14px 60px}.card{padding:20px 18px}.prog-line{width:32px}.header-inner{padding:12px 14px}}
 
 
 
 
 
promptforge_fixed.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2e276ed5b093d1d0f9a16457518c2b36ae9d1a8cfdd37f220b8c4fb724f29c77
3
+ size 35095
upload.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cc774d5119d3387f0c082491037886c74640235b9e512e3e6186ad53c920f1da
3
+ size 34298