Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<your-token-here>" > .env.local
````
1. Install dependencies and run the server:
```
npm install
Expand Down
8 changes: 7 additions & 1 deletion pages/api/predictions/[id].js
Original file line number Diff line number Diff line change
@@ -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",
},
});
Expand Down
32 changes: 12 additions & 20 deletions pages/api/predictions/index.js
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
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});

res.statusCode = 201;
res.end(JSON.stringify(prediction));
}
Expand Down
149 changes: 112 additions & 37 deletions pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,67 @@ 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 (
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-auto">
<form
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"
onSubmit={e => {
e.preventDefault();
if (token) {
localStorage.setItem("replicateApiToken", token);
onTokenSet(token);
}
}}
aria-modal="true"
role="dialog"
>
<h2 className="text-2xl font-bold mb-2 text-center">Enter your Replicate API token</h2>
<p className="mb-4 text-center text-gray-600">
Get a free token at {" "}
<a
href="https://replicate.com/account/api-tokens?new-token-name=paint-by-text-kontext"
target="_blank"
rel="noopener noreferrer"
className="underline text-blue-600"
>
replicate.com/account/api-tokens
</a>
</p>
<input
className="w-full border rounded p-3 mb-4 text-lg"
type="text"
value={token}
onChange={e => setToken(e.target.value)}
placeholder="r8_..."
required
autoFocus
/>
<button className="w-full bg-black text-white px-4 py-3 rounded text-lg font-semibold" type="submit">
Start painting
</button>
</form>
</div>
);
}

export default function Home() {
const [events, setEvents] = useState([]);
const [predictions, setPredictions] = useState([]);
const [error, setError] = useState(null);
const [isProcessing, setIsProcessing] = useState(false);
const [seed] = useState(getRandomSeed());
const [initialPrompt, setInitialPrompt] = useState(seed.prompt);
const [apiToken, setApiToken] = useState(null);
// 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);
// Removed setShowTokenForm
}, [seed.image]);

const handleImageDropped = async (image) => {
Expand All @@ -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;
Expand All @@ -60,6 +111,7 @@ export default function Home() {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-replicate-api-token": apiToken,
},
body: JSON.stringify(body),
});
Expand All @@ -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);
Expand Down Expand Up @@ -105,8 +159,19 @@ export default function Home() {
setInitialPrompt(seed.prompt);
};

const handleTokenSet = (token) => {
setApiToken(token);
// Removed setShowTokenForm
};

const handleLogout = () => {
localStorage.removeItem("replicateApiToken");
setApiToken(null);
// Removed setShowTokenForm
};

return (
<div>
<div className="relative">
<Head>
<title>{appName}</title>
<meta name="description" content={appMetaDescription} />
Expand All @@ -115,42 +180,52 @@ export default function Home() {
<meta property="og:image" content="https://paintbytext.chat/opengraph.jpg" />
</Head>

<main className="container max-w-[700px] mx-auto p-5">
<hgroup>
<h1 className="text-center text-5xl font-bold m-6">{appName}</h1>
<p className="text-center text-xl opacity-60 m-6">
{appSubtitle}
</p>
</hgroup>

<Messages
events={events}
isProcessing={isProcessing}
onUndo={(index) => {
setInitialPrompt(events[index - 1].prompt);
setEvents(
events.slice(0, index - 1).concat(events.slice(index + 1))
);
}}
/>

<PromptForm
initialPrompt={initialPrompt}
isFirstPrompt={events.length === 1}
onSubmit={handleSubmit}
disabled={isProcessing}
/>

<div className="mx-auto w-full">
{error && <p className="bold text-red-500 pb-5">{error}</p>}
</div>

<Footer
events={events}
startOver={startOver}
handleImageDropped={handleImageDropped}
/>
<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' : ''}`}>
{!apiToken ? null : (
<>
<div className="flex justify-end mb-4">
<button className="text-sm underline text-blue-600" onClick={handleLogout}>
Log out / Change token
</button>
</div>
<hgroup>
<h1 className="text-center text-5xl font-bold m-6">{appName}</h1>
<p className="text-center text-xl opacity-60 m-6">
{appSubtitle}
</p>
</hgroup>

<Messages
events={events}
isProcessing={isProcessing}
onUndo={(index) => {
setInitialPrompt(events[index - 1].prompt);
setEvents(
events.slice(0, index - 1).concat(events.slice(index + 1))
);
}}
/>

<PromptForm
initialPrompt={initialPrompt}
isFirstPrompt={events.length === 1}
onSubmit={handleSubmit}
disabled={isProcessing}
/>

<div className="mx-auto w-full">
{error && <p className="bold text-red-500 pb-5">{error}</p>}
</div>

<Footer
events={events}
startOver={startOver}
handleImageDropped={handleImageDropped}
/>
</>
)}
</main>
{!apiToken && <TokenModal onTokenSet={handleTokenSet} />}
</div>
);
}