Spaces:
Runtime error
Runtime error
| const API = ""; | |
| const $ = id => document.getElementById(id); | |
| let currentPromptId = null; | |
| function toast(msg, type = "info") { | |
| const el = $("toast"); | |
| el.textContent = `✅ ${msg}`; | |
| el.classList.add("show"); | |
| setTimeout(() => el.classList.remove("show"), 3000); | |
| } | |
| async function apiFetch(path, method = "GET", body = null) { | |
| const opts = { method, headers: { "Content-Type": "application/json" } }; | |
| if (body) opts.body = JSON.stringify(body); | |
| const r = await fetch(API + path, opts); | |
| if (!r.ok) { | |
| const e = await r.json().catch(() => ({ detail: r.statusText })); | |
| throw new Error(e.detail || "Request failed"); | |
| } | |
| return r.json(); | |
| } | |
| // ========== API Key Management with localStorage ========== | |
| const STORAGE_KEY = "promptforge_hf_key"; | |
| function loadSavedKey() { | |
| const saved = localStorage.getItem(STORAGE_KEY); | |
| if (saved) { | |
| $("hf-key").value = saved; | |
| updateKeyStatus(saved.length >= 10); | |
| } | |
| } | |
| function saveKey(key) { | |
| if (key && key.length >= 10) { | |
| localStorage.setItem(STORAGE_KEY, key); | |
| } else { | |
| localStorage.removeItem(STORAGE_KEY); | |
| } | |
| } | |
| function updateKeyStatus(hasKey) { | |
| $("check-key").disabled = !hasKey; | |
| $("key-dot").className = "dot" + (hasKey ? " ok" : ""); | |
| $("key-status-text").textContent = hasKey ? "Key saved" : "No key saved"; | |
| } | |
| loadSavedKey(); | |
| // Toggle key section visibility | |
| function toggleKeySection() { | |
| const body = $("key-body"); | |
| const chevron = $("key-chevron").parentElement; // کلید header | |
| body.classList.toggle("hidden"); | |
| chevron.classList.toggle("open"); | |
| } | |
| window.toggleKeySection = toggleKeySection; // برای onclick در HTML | |
| $("toggle-key").addEventListener("click", () => { | |
| const input = $("hf-key"); | |
| input.type = input.type === "password" ? "text" : "password"; | |
| }); | |
| $("hf-key").addEventListener("input", (e) => { | |
| const val = e.target.value.trim(); | |
| updateKeyStatus(val.length >= 10); | |
| // ذخیره خودکار بعد از تأیید (اما فعلاً فقط وضعیت) | |
| }); | |
| $("check-key").addEventListener("click", async () => { | |
| const key = $("hf-key").value.trim(); | |
| if (!key) return; | |
| const btn = $("check-key"); | |
| btn.disabled = true; | |
| btn.innerHTML = `<span class="spinner"></span>`; | |
| try { | |
| const res = await apiFetch("/api/check-key", "POST", { provider: "huggingface", api_key: key }); | |
| if (res.valid) { | |
| $("key-dot").className = "dot ok"; | |
| $("key-status-text").textContent = "Valid – saved"; | |
| saveKey(key); | |
| } else { | |
| $("key-dot").className = "dot err"; | |
| $("key-status-text").textContent = "Invalid"; | |
| localStorage.removeItem(STORAGE_KEY); | |
| } | |
| toast(res.message, res.valid ? "success" : "error"); | |
| } catch (e) { | |
| $("key-dot").className = "dot err"; | |
| $("key-status-text").textContent = "Check failed"; | |
| toast("Check failed: " + e.message, "error"); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>`; | |
| } | |
| }); | |
| $("clear-key").addEventListener("click", () => { | |
| localStorage.removeItem(STORAGE_KEY); | |
| $("hf-key").value = ""; | |
| updateKeyStatus(false); | |
| toast("Key cleared", "info"); | |
| }); | |
| // ========== Speech Recognition (Persian) ========== | |
| const micBtn = $("mic-btn"); | |
| const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| if (SpeechRecognition) { | |
| const recognition = new SpeechRecognition(); | |
| recognition.lang = "fa-IR"; | |
| recognition.continuous = false; | |
| recognition.interimResults = false; | |
| micBtn.addEventListener("click", () => { | |
| recognition.start(); | |
| micBtn.classList.add("recording"); | |
| }); | |
| recognition.addEventListener("result", (e) => { | |
| const text = e.results[0][0].transcript; | |
| $("instruction").value += text; | |
| }); | |
| recognition.addEventListener("end", () => micBtn.classList.remove("recording")); | |
| recognition.addEventListener("error", () => micBtn.classList.remove("recording")); | |
| } else { | |
| micBtn.style.opacity = "0.3"; | |
| micBtn.title = "Speech not supported in this browser"; | |
| } | |
| // ========== Generate ========== | |
| $("generate-btn").addEventListener("click", async () => { | |
| const instruction = $("instruction").value.trim(); | |
| if (!instruction) { toast("Please enter an instruction", "error"); return; } | |
| const targetModel = $("target-model").value; | |
| const apiKey = $("hf-key").value.trim() || null; // از input (که با localStorage پر شده) | |
| const btn = $("generate-btn"); | |
| btn.disabled = true; | |
| btn.innerHTML = `<span class="spinner"></span> Generating...`; | |
| try { | |
| const data = await apiFetch("/api/generate", "POST", { | |
| instruction, | |
| target_model: targetModel, | |
| provider: "huggingface", | |
| api_key: apiKey, | |
| enhance: !!apiKey, | |
| output_format: "both", | |
| persona: "default", | |
| style: "professional", | |
| user_constraints: [] | |
| }); | |
| currentPromptId = data.prompt_id; | |
| renderOutput(data.manifest.structured_prompt); | |
| $("output-area").classList.remove("hidden"); | |
| toast("Prompt generated!", "success"); | |
| } catch (e) { | |
| toast(`Error: ${e.message}`, "error"); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> Generate Prompt`; | |
| } | |
| }); | |
| function renderOutput(sp) { | |
| const container = $("quad-sections"); | |
| const sections = [ | |
| { title: "ROLE", content: sp.role }, | |
| { title: "TASK", content: sp.task }, | |
| { title: "INPUT FORMAT", content: sp.input_format }, | |
| { title: "OUTPUT FORMAT", content: sp.output_format }, | |
| { title: "CONSTRAINTS", content: sp.constraints.join("\n") }, | |
| { title: "SAFETY", content: sp.safety.join("\n") } | |
| ]; | |
| container.innerHTML = sections.map(s => ` | |
| <div class="quad-section"> | |
| <div class="section-header"> | |
| <strong>${s.title}</strong> | |
| <button class="copy-section" data-content="${escapeHtml(s.content)}">📋 Copy</button> | |
| </div> | |
| <pre class="section-content">${escapeHtml(s.content)}</pre> | |
| </div> | |
| `).join(""); | |
| // Copy individual section | |
| document.querySelectorAll(".copy-section").forEach(btn => { | |
| btn.addEventListener("click", () => { | |
| navigator.clipboard.writeText(btn.dataset.content); | |
| btn.textContent = "✅"; | |
| setTimeout(() => btn.textContent = "📋 Copy", 1500); | |
| }); | |
| }); | |
| // Copy all | |
| $("copy-all").addEventListener("click", () => { | |
| const full = sections.map(s => `## ${s.title}\n${s.content}`).join("\n\n"); | |
| navigator.clipboard.writeText(full); | |
| toast("Full prompt copied!", "success"); | |
| }); | |
| // Export JSON | |
| $("export-json").addEventListener("click", () => { | |
| const json = JSON.stringify(sp, null, 2); | |
| const blob = new Blob([json], { type: "application/json" }); | |
| const a = document.createElement("a"); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = `prompt-${currentPromptId.slice(0,8)}.json`; | |
| a.click(); | |
| URL.revokeObjectURL(a.href); | |
| }); | |
| } | |
| function escapeHtml(unsafe) { | |
| return unsafe.replace(/[&<>"]/g, function(m) { | |
| if (m === "&") return "&"; | |
| if (m === "<") return "<"; | |
| if (m === ">") return ">"; | |
| if (m === '"') return """; | |
| return m; | |
| }); | |
| } |