From c535811a67719ac70863b8bf55a7270a31c6fc6b Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Mon, 7 Jul 2025 08:21:41 -0700 Subject: [PATCH 1/2] bring your own token --- README.md | 11 ++- pages/api/predictions/[id].js | 8 +- pages/api/predictions/index.js | 33 +++----- pages/index.js | 149 +++++++++++++++++++++++++-------- 4 files changed, 139 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index 7b1ec4c..31e27f0 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,16 @@ This app is powered by: 🍃 [Tailwind CSS](https://tailwindcss.com/), for styles. +## Usage + +1. Open the app in your browser. +1. When prompted, enter your [Replicate API token](https://replicate.com/account/api-tokens?new-token-name=paint-by-text-kontext). +1. You can generate a free token at the link above (requires a Replicate account). +1. Your token is stored securely in your browser and used only for your requests. + ## Development 1. Install a recent version of [Node.js](https://nodejs.org/) -1. Copy your [Replicate API token](https://replicate.com/account?utm_source=project&utm_campaign=paintbytext) and set it in your environment: - ``` - echo "REPLICATE_API_TOKEN=" > .env.local - ```` 1. Install dependencies and run the server: ``` npm install diff --git a/pages/api/predictions/[id].js b/pages/api/predictions/[id].js index 7029fbb..bba1aa4 100644 --- a/pages/api/predictions/[id].js +++ b/pages/api/predictions/[id].js @@ -1,9 +1,15 @@ const API_HOST = process.env.REPLICATE_API_HOST || "https://api.replicate.com"; export default async function handler(req, res) { + const token = req.headers["x-replicate-api-token"]; + if (!token) { + res.statusCode = 401; + res.end(JSON.stringify({ detail: "Missing Replicate API token. Please provide your token in the x-replicate-api-token header." })); + return; + } const response = await fetch(`${API_HOST}/v1/predictions/${req.query.id}`, { headers: { - Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, + Authorization: `Token ${token}`, "Content-Type": "application/json", }, }); diff --git a/pages/api/predictions/index.js b/pages/api/predictions/index.js index 7f8447e..7c85882 100644 --- a/pages/api/predictions/index.js +++ b/pages/api/predictions/index.js @@ -1,38 +1,31 @@ import Replicate from "replicate"; - -const replicate = new Replicate({ - auth: process.env.REPLICATE_API_TOKEN, - userAgent: `${packageData.name}/${packageData.version}` -}); - - +import packageData from "../../../package.json"; const API_HOST = process.env.REPLICATE_API_HOST || "https://api.replicate.com"; -import packageData from "../../../package.json"; - export default async function handler(req, res) { - if (!process.env.REPLICATE_API_TOKEN) { - throw new Error("The REPLICATE_API_TOKEN environment variable is not set. See README.md for instructions on how to set it."); + const token = req.headers["x-replicate-api-token"]; + if (!token) { + res.statusCode = 401; + res.end(JSON.stringify({ detail: "Missing Replicate API token. Please provide your token in the x-replicate-api-token header." })); + return; } - // remove null and undefined values req.body = Object.entries(req.body).reduce( (a, [k, v]) => (v == null ? a : ((a[k] = v), a)), {} ); - - let prediction - - const model = "black-forest-labs/flux-kontext-pro" + let prediction; + const model = "black-forest-labs/flux-kontext-pro"; + const replicate = new Replicate({ + auth: token, + userAgent: `${packageData.name}/${packageData.version}` + }); prediction = await replicate.predictions.create({ model, input: req.body }); - - - console.log({prediction}); - + console.log({ prediction }); res.statusCode = 201; res.end(JSON.stringify(prediction)); } diff --git a/pages/index.js b/pages/index.js index 422f2b5..a7cf13a 100644 --- a/pages/index.js +++ b/pages/index.js @@ -14,6 +14,51 @@ export const appName = "Paint by Text"; export const appSubtitle = "Edit your photos using written instructions, with the help of an AI."; export const appMetaDescription = "Edit your photos using written instructions, with the help of an AI."; +function TokenModal({ onTokenSet }) { + const [token, setToken] = useState(""); + return ( +
+
{ + e.preventDefault(); + if (token) { + localStorage.setItem("replicateApiToken", token); + onTokenSet(token); + } + }} + aria-modal="true" + role="dialog" + > +

Enter your Replicate API token

+

+ Get a free token at {" "} + + replicate.com/account/api-tokens + +

+ setToken(e.target.value)} + placeholder="r8_..." + required + autoFocus + /> + +
+
+ ); +} + export default function Home() { const [events, setEvents] = useState([]); const [predictions, setPredictions] = useState([]); @@ -21,10 +66,15 @@ export default function Home() { const [isProcessing, setIsProcessing] = useState(false); const [seed] = useState(getRandomSeed()); const [initialPrompt, setInitialPrompt] = useState(seed.prompt); + const [apiToken, setApiToken] = useState(null); + const [showTokenForm, setShowTokenForm] = useState(true); // set the initial image from a random seed useEffect(() => { setEvents([{ image: seed.image }]); + const storedToken = localStorage.getItem("replicateApiToken"); + if (storedToken) setApiToken(storedToken); + setShowTokenForm(true); }, [seed.image]); const handleImageDropped = async (image) => { @@ -39,6 +89,7 @@ export default function Home() { const handleSubmit = async (e) => { e.preventDefault(); + if (!apiToken) return; const prompt = e.target.prompt.value; const lastImage = events.findLast((ev) => ev.image)?.image; @@ -60,6 +111,7 @@ export default function Home() { method: "POST", headers: { "Content-Type": "application/json", + "x-replicate-api-token": apiToken, }, body: JSON.stringify(body), }); @@ -75,7 +127,9 @@ export default function Home() { prediction.status !== "failed" ) { await sleep(500); - const response = await fetch("/api/predictions/" + prediction.id); + const response = await fetch("/api/predictions/" + prediction.id, { + headers: { "x-replicate-api-token": apiToken }, + }); prediction = await response.json(); if (response.status !== 200) { setError(prediction.detail); @@ -105,8 +159,19 @@ export default function Home() { setInitialPrompt(seed.prompt); }; + const handleTokenSet = (token) => { + setApiToken(token); + setShowTokenForm(false); + }; + + const handleLogout = () => { + localStorage.removeItem("replicateApiToken"); + setApiToken(null); + setShowTokenForm(true); + }; + return ( -
+
{appName} @@ -115,42 +180,52 @@ export default function Home() { -
-
-

{appName}

-

- {appSubtitle} -

-
- - { - setInitialPrompt(events[index - 1].prompt); - setEvents( - events.slice(0, index - 1).concat(events.slice(index + 1)) - ); - }} - /> - - - -
- {error &&

{error}

} -
- -
+
+ {showTokenForm ? null : ( + <> +
+ +
+
+

{appName}

+

+ {appSubtitle} +

+
+ + { + setInitialPrompt(events[index - 1].prompt); + setEvents( + events.slice(0, index - 1).concat(events.slice(index + 1)) + ); + }} + /> + + + +
+ {error &&

{error}

} +
+ +
+ + )}
+ {showTokenForm && }
); } From 35db49a54f15ff71241eb907a2e67394a187ffa4 Mon Sep 17 00:00:00 2001 From: Zeke Sikelianos Date: Mon, 7 Jul 2025 17:55:17 -0700 Subject: [PATCH 2/2] simplify state --- pages/api/predictions/index.js | 1 - pages/index.js | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pages/api/predictions/index.js b/pages/api/predictions/index.js index 7c85882..27d8ac4 100644 --- a/pages/api/predictions/index.js +++ b/pages/api/predictions/index.js @@ -25,7 +25,6 @@ export default async function handler(req, res) { model, input: req.body }); - console.log({ prediction }); res.statusCode = 201; res.end(JSON.stringify(prediction)); } diff --git a/pages/index.js b/pages/index.js index a7cf13a..ae114ee 100644 --- a/pages/index.js +++ b/pages/index.js @@ -67,14 +67,14 @@ export default function Home() { const [seed] = useState(getRandomSeed()); const [initialPrompt, setInitialPrompt] = useState(seed.prompt); const [apiToken, setApiToken] = useState(null); - const [showTokenForm, setShowTokenForm] = useState(true); +// Removed showTokenForm state // set the initial image from a random seed useEffect(() => { setEvents([{ image: seed.image }]); const storedToken = localStorage.getItem("replicateApiToken"); if (storedToken) setApiToken(storedToken); - setShowTokenForm(true); + // Removed setShowTokenForm }, [seed.image]); const handleImageDropped = async (image) => { @@ -161,13 +161,13 @@ export default function Home() { const handleTokenSet = (token) => { setApiToken(token); - setShowTokenForm(false); + // Removed setShowTokenForm }; const handleLogout = () => { localStorage.removeItem("replicateApiToken"); setApiToken(null); - setShowTokenForm(true); + // Removed setShowTokenForm }; return ( @@ -180,8 +180,8 @@ export default function Home() { -
- {showTokenForm ? null : ( +
+ {!apiToken ? null : ( <>
- {showTokenForm && } + {!apiToken && }
); }