Skip to content

Commit 813a3df

Browse files
authored
Merge pull request #29 from replicate/bring-your-own-token
bring your own token
2 parents 4c5c6a7 + 35db49a commit 813a3df

4 files changed

Lines changed: 138 additions & 62 deletions

File tree

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@ This app is powered by:
2121
🍃 [Tailwind CSS](https://tailwindcss.com/), for styles.
2222

2323

24+
## Usage
25+
26+
1. Open the app in your browser.
27+
1. When prompted, enter your [Replicate API token](https://replicate.com/account/api-tokens?new-token-name=paint-by-text-kontext).
28+
1. You can generate a free token at the link above (requires a Replicate account).
29+
1. Your token is stored securely in your browser and used only for your requests.
30+
2431
## Development
2532

2633
1. Install a recent version of [Node.js](https://nodejs.org/)
27-
1. Copy your [Replicate API token](https://replicate.com/account?utm_source=project&utm_campaign=paintbytext) and set it in your environment:
28-
```
29-
echo "REPLICATE_API_TOKEN=<your-token-here>" > .env.local
30-
````
3134
1. Install dependencies and run the server:
3235
```
3336
npm install

pages/api/predictions/[id].js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
const API_HOST = process.env.REPLICATE_API_HOST || "https://api.replicate.com";
22

33
export default async function handler(req, res) {
4+
const token = req.headers["x-replicate-api-token"];
5+
if (!token) {
6+
res.statusCode = 401;
7+
res.end(JSON.stringify({ detail: "Missing Replicate API token. Please provide your token in the x-replicate-api-token header." }));
8+
return;
9+
}
410
const response = await fetch(`${API_HOST}/v1/predictions/${req.query.id}`, {
511
headers: {
6-
Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`,
12+
Authorization: `Token ${token}`,
713
"Content-Type": "application/json",
814
},
915
});

pages/api/predictions/index.js

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,30 @@
11
import Replicate from "replicate";
2-
3-
const replicate = new Replicate({
4-
auth: process.env.REPLICATE_API_TOKEN,
5-
userAgent: `${packageData.name}/${packageData.version}`
6-
});
7-
8-
2+
import packageData from "../../../package.json";
93

104
const API_HOST = process.env.REPLICATE_API_HOST || "https://api.replicate.com";
115

12-
import packageData from "../../../package.json";
13-
146
export default async function handler(req, res) {
15-
if (!process.env.REPLICATE_API_TOKEN) {
16-
throw new Error("The REPLICATE_API_TOKEN environment variable is not set. See README.md for instructions on how to set it.");
7+
const token = req.headers["x-replicate-api-token"];
8+
if (!token) {
9+
res.statusCode = 401;
10+
res.end(JSON.stringify({ detail: "Missing Replicate API token. Please provide your token in the x-replicate-api-token header." }));
11+
return;
1712
}
18-
1913
// remove null and undefined values
2014
req.body = Object.entries(req.body).reduce(
2115
(a, [k, v]) => (v == null ? a : ((a[k] = v), a)),
2216
{}
2317
);
24-
25-
let prediction
26-
27-
const model = "black-forest-labs/flux-kontext-pro"
18+
let prediction;
19+
const model = "black-forest-labs/flux-kontext-pro";
20+
const replicate = new Replicate({
21+
auth: token,
22+
userAgent: `${packageData.name}/${packageData.version}`
23+
});
2824
prediction = await replicate.predictions.create({
2925
model,
3026
input: req.body
3127
});
32-
33-
34-
console.log({prediction});
35-
3628
res.statusCode = 201;
3729
res.end(JSON.stringify(prediction));
3830
}

pages/index.js

Lines changed: 112 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,67 @@ export const appName = "Paint by Text";
1414
export const appSubtitle = "Edit your photos using written instructions, with the help of an AI.";
1515
export const appMetaDescription = "Edit your photos using written instructions, with the help of an AI.";
1616

17+
function TokenModal({ onTokenSet }) {
18+
const [token, setToken] = useState("");
19+
return (
20+
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-auto">
21+
<form
22+
className="w-full max-w-md mx-auto p-8 bg-white rounded-xl shadow-2xl border border-gray-200 relative animate-in fade-in"
23+
onSubmit={e => {
24+
e.preventDefault();
25+
if (token) {
26+
localStorage.setItem("replicateApiToken", token);
27+
onTokenSet(token);
28+
}
29+
}}
30+
aria-modal="true"
31+
role="dialog"
32+
>
33+
<h2 className="text-2xl font-bold mb-2 text-center">Enter your Replicate API token</h2>
34+
<p className="mb-4 text-center text-gray-600">
35+
Get a free token at {" "}
36+
<a
37+
href="https://replicate.com/account/api-tokens?new-token-name=paint-by-text-kontext"
38+
target="_blank"
39+
rel="noopener noreferrer"
40+
className="underline text-blue-600"
41+
>
42+
replicate.com/account/api-tokens
43+
</a>
44+
</p>
45+
<input
46+
className="w-full border rounded p-3 mb-4 text-lg"
47+
type="text"
48+
value={token}
49+
onChange={e => setToken(e.target.value)}
50+
placeholder="r8_..."
51+
required
52+
autoFocus
53+
/>
54+
<button className="w-full bg-black text-white px-4 py-3 rounded text-lg font-semibold" type="submit">
55+
Start painting
56+
</button>
57+
</form>
58+
</div>
59+
);
60+
}
61+
1762
export default function Home() {
1863
const [events, setEvents] = useState([]);
1964
const [predictions, setPredictions] = useState([]);
2065
const [error, setError] = useState(null);
2166
const [isProcessing, setIsProcessing] = useState(false);
2267
const [seed] = useState(getRandomSeed());
2368
const [initialPrompt, setInitialPrompt] = useState(seed.prompt);
69+
const [apiToken, setApiToken] = useState(null);
70+
// Removed showTokenForm state
2471

2572
// set the initial image from a random seed
2673
useEffect(() => {
2774
setEvents([{ image: seed.image }]);
75+
const storedToken = localStorage.getItem("replicateApiToken");
76+
if (storedToken) setApiToken(storedToken);
77+
// Removed setShowTokenForm
2878
}, [seed.image]);
2979

3080
const handleImageDropped = async (image) => {
@@ -39,6 +89,7 @@ export default function Home() {
3989

4090
const handleSubmit = async (e) => {
4191
e.preventDefault();
92+
if (!apiToken) return;
4293

4394
const prompt = e.target.prompt.value;
4495
const lastImage = events.findLast((ev) => ev.image)?.image;
@@ -60,6 +111,7 @@ export default function Home() {
60111
method: "POST",
61112
headers: {
62113
"Content-Type": "application/json",
114+
"x-replicate-api-token": apiToken,
63115
},
64116
body: JSON.stringify(body),
65117
});
@@ -75,7 +127,9 @@ export default function Home() {
75127
prediction.status !== "failed"
76128
) {
77129
await sleep(500);
78-
const response = await fetch("/api/predictions/" + prediction.id);
130+
const response = await fetch("/api/predictions/" + prediction.id, {
131+
headers: { "x-replicate-api-token": apiToken },
132+
});
79133
prediction = await response.json();
80134
if (response.status !== 200) {
81135
setError(prediction.detail);
@@ -105,8 +159,19 @@ export default function Home() {
105159
setInitialPrompt(seed.prompt);
106160
};
107161

162+
const handleTokenSet = (token) => {
163+
setApiToken(token);
164+
// Removed setShowTokenForm
165+
};
166+
167+
const handleLogout = () => {
168+
localStorage.removeItem("replicateApiToken");
169+
setApiToken(null);
170+
// Removed setShowTokenForm
171+
};
172+
108173
return (
109-
<div>
174+
<div className="relative">
110175
<Head>
111176
<title>{appName}</title>
112177
<meta name="description" content={appMetaDescription} />
@@ -115,42 +180,52 @@ export default function Home() {
115180
<meta property="og:image" content="https://paintbytext.chat/opengraph.jpg" />
116181
</Head>
117182

118-
<main className="container max-w-[700px] mx-auto p-5">
119-
<hgroup>
120-
<h1 className="text-center text-5xl font-bold m-6">{appName}</h1>
121-
<p className="text-center text-xl opacity-60 m-6">
122-
{appSubtitle}
123-
</p>
124-
</hgroup>
125-
126-
<Messages
127-
events={events}
128-
isProcessing={isProcessing}
129-
onUndo={(index) => {
130-
setInitialPrompt(events[index - 1].prompt);
131-
setEvents(
132-
events.slice(0, index - 1).concat(events.slice(index + 1))
133-
);
134-
}}
135-
/>
136-
137-
<PromptForm
138-
initialPrompt={initialPrompt}
139-
isFirstPrompt={events.length === 1}
140-
onSubmit={handleSubmit}
141-
disabled={isProcessing}
142-
/>
143-
144-
<div className="mx-auto w-full">
145-
{error && <p className="bold text-red-500 pb-5">{error}</p>}
146-
</div>
147-
148-
<Footer
149-
events={events}
150-
startOver={startOver}
151-
handleImageDropped={handleImageDropped}
152-
/>
183+
<main className={`container max-w-[700px] mx-auto p-5 transition-filter duration-300 ${!apiToken ? 'filter blur-sm brightness-75 pointer-events-none select-none' : ''}`}>
184+
{!apiToken ? null : (
185+
<>
186+
<div className="flex justify-end mb-4">
187+
<button className="text-sm underline text-blue-600" onClick={handleLogout}>
188+
Log out / Change token
189+
</button>
190+
</div>
191+
<hgroup>
192+
<h1 className="text-center text-5xl font-bold m-6">{appName}</h1>
193+
<p className="text-center text-xl opacity-60 m-6">
194+
{appSubtitle}
195+
</p>
196+
</hgroup>
197+
198+
<Messages
199+
events={events}
200+
isProcessing={isProcessing}
201+
onUndo={(index) => {
202+
setInitialPrompt(events[index - 1].prompt);
203+
setEvents(
204+
events.slice(0, index - 1).concat(events.slice(index + 1))
205+
);
206+
}}
207+
/>
208+
209+
<PromptForm
210+
initialPrompt={initialPrompt}
211+
isFirstPrompt={events.length === 1}
212+
onSubmit={handleSubmit}
213+
disabled={isProcessing}
214+
/>
215+
216+
<div className="mx-auto w-full">
217+
{error && <p className="bold text-red-500 pb-5">{error}</p>}
218+
</div>
219+
220+
<Footer
221+
events={events}
222+
startOver={startOver}
223+
handleImageDropped={handleImageDropped}
224+
/>
225+
</>
226+
)}
153227
</main>
228+
{!apiToken && <TokenModal onTokenSet={handleTokenSet} />}
154229
</div>
155230
);
156231
}

0 commit comments

Comments
 (0)