diff --git a/backend.js b/backend.js index a43b137..106e019 100644 --- a/backend.js +++ b/backend.js @@ -6,6 +6,7 @@ import express from "express"; import promptsRouter from "./routes/prompts.js"; import rateRouter from "./routes/rate.js"; import deleteRouter from "./routes/delete.js"; +import generateImageRouter from "./routes/generate_image.js"; /* CREATE PAGE */ import createRouter from "./routes/create.js"; @@ -28,6 +29,7 @@ app.use(express.static("frontend")); app.use("/api", promptsRouter); app.use("/api", rateRouter); app.use("/api", deleteRouter); +app.use("/api", generateImageRouter); /* CREATE PAGE */ app.use("/api", createRouter); diff --git a/frontend/css/prompts.css b/frontend/css/prompts.css index daa26a4..4fede48 100644 --- a/frontend/css/prompts.css +++ b/frontend/css/prompts.css @@ -315,3 +315,48 @@ background: #6f42c1; color: #fff; } + +/* Image Preview */ +.preview-btn { + background: transparent; + border: 1.5px solid #a78bfa; + color: #a78bfa; + border-radius: 9999px; + padding: 0.3rem 1rem; + font-size: 0.8rem; + cursor: pointer; + transition: background 0.2s, color 0.2s; +} + +.preview-btn:hover:not(:disabled) { + background: #a78bfa; + color: #fff; +} + +.preview-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.image-preview-container { + margin-top: 0.75rem; +} + +.preview-img { + max-width: 100%; + max-height: 320px; + border-radius: 8px; + border: 1px solid #a78bfa44; + display: block; +} + +.preview-loading { + font-size: 0.85rem; + color: #a78bfa; + font-style: italic; +} + +.preview-error { + font-size: 0.85rem; + color: #f87171; +} diff --git a/frontend/js/prompts.js b/frontend/js/prompts.js index f2bd54d..7c69c5a 100644 --- a/frontend/js/prompts.js +++ b/frontend/js/prompts.js @@ -241,6 +241,44 @@ function openDeleteModal(promptId) { document.body.appendChild(overlay); } +async function generatePreview(promptText, btn, container) { + if (container.querySelector(".preview-img")) { + container.innerHTML = ""; + btn.textContent = "Preview Image"; + return; + } + + btn.disabled = true; + btn.textContent = "Generating..."; + container.innerHTML = 'Generating image, please wait...'; + + try { + const res = await fetch("/api/generate-image", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ prompt: promptText }), + }); + + const data = await res.json(); + + if (!res.ok || data.error) { + container.innerHTML = `Could not generate image: ${data.error}`; + } else { + container.innerHTML = ""; + const img = document.createElement("img"); + img.src = data.image; + img.alt = "AI generated preview"; + img.className = "preview-img"; + container.appendChild(img); + btn.textContent = "Hide Preview"; + } + } catch (err) { + container.innerHTML = `Error: ${err.message}`; + } finally { + btn.disabled = false; + } +} + function buildPromptRow(prompt) { const row = document.createElement("div"); row.className = "prompt-row"; @@ -295,14 +333,24 @@ function buildPromptRow(prompt) { tags.appendChild(tag); } + const previewBtn = document.createElement("button"); + previewBtn.className = "preview-btn"; + previewBtn.textContent = "Preview Image"; + previewBtn.addEventListener("click", () => generatePreview(prompt.prompt, previewBtn, imageContainer)); + + const imageContainer = document.createElement("div"); + imageContainer.className = "image-preview-container"; + actions.appendChild(rateLabel); actions.appendChild(stars); actions.appendChild(submitBtn); + actions.appendChild(previewBtn); actions.appendChild(deleteBtn); actions.appendChild(tags); contentCol.appendChild(text); contentCol.appendChild(actions); + contentCol.appendChild(imageContainer); row.appendChild(ratingCol); row.appendChild(contentCol); diff --git a/routes/generate_image.js b/routes/generate_image.js new file mode 100644 index 0000000..88c1095 --- /dev/null +++ b/routes/generate_image.js @@ -0,0 +1,38 @@ +import express from "express"; + +const router = express.Router(); + +router.post("/generate-image", async (req, res) => { + const { prompt } = req.body; + if (!prompt) return res.status(400).json({ error: "prompt required" }); + + const token = process.env.HF_TOKEN; + if (!token) return res.status(500).json({ error: "HF_TOKEN not set" }); + + try { + const hfRes = await fetch( + "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell", + { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ inputs: prompt }), + }, + ); + + if (!hfRes.ok) { + const msg = await hfRes.text(); + return res.status(hfRes.status).json({ error: msg }); + } + + const buffer = await hfRes.arrayBuffer(); + const base64 = Buffer.from(buffer).toString("base64"); + res.json({ image: `data:image/jpeg;base64,${base64}` }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +export default router;