Skip to content
Open
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
10 changes: 2 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@ To run this app locally or deploy your own copy, you'll need:
- Node.js (20 or later)
- A [Replicate API token](https://replicate.com/account/api-tokens)

Set your Replicate API token in your environment:

```sh
export REPLICATE_API_TOKEN=r8_...
```

Then install dependencies and run the development server:
Install dependencies and run the development server:

```
npm install && npm run dev
```

Then open [localhost:3000](http://localhost:3000) in your browser to see the app.
Then open [localhost:3000](http://localhost:3000) in your browser. You'll be prompted to enter your Replicate API token, which will be stored in your browser's local storage for future use.

## Continuous Deployment

Expand Down
49 changes: 34 additions & 15 deletions app/api/generate-image/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ import { NextResponse } from "next/server";
import Replicate from "replicate";

export async function GET(request: Request) {
const replicate = new Replicate();
const { searchParams } = new URL(request.url);
const prompt = searchParams.get("text");
const token = searchParams.get("token");

if (!token) {
return new NextResponse("API token is required", {
status: 401,
statusText: "Unauthorized",
});
}

const replicate = new Replicate({
auth: token,
});

const model = "black-forest-labs/flux-schnell";
const input = {
Expand All @@ -18,18 +29,26 @@ export async function GET(request: Request) {
num_inference_steps: 2,
};

const output = await replicate.run(model, { input }) as string[];
const headers = new Headers();
headers.set("Content-Type", "image/*");
headers.set(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate"
);
headers.set("Pragma", "no-cache");
headers.set("Expires", "0");
return new NextResponse(output[0], {
status: 200,
statusText: "OK",
headers,
});
try {
const output = await replicate.run(model, { input }) as string[];
const headers = new Headers();
headers.set("Content-Type", "image/*");
headers.set(
"Cache-Control",
"no-store, no-cache, must-revalidate, proxy-revalidate"
);
headers.set("Pragma", "no-cache");
headers.set("Expires", "0");
return new NextResponse(output[0], {
status: 200,
statusText: "OK",
headers,
});
} catch (error) {
console.error("Error generating image:", error);
return new NextResponse("Failed to generate image. Please check your API token.", {
status: 500,
statusText: "Internal Server Error",
});
}
}
126 changes: 116 additions & 10 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,119 @@ import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import Link from "next/link";

const DEFAULT_INPUT = "super fast pink and gold jet is flying against a beautiful nebula, trail behind it"
const DEFAULT_INPUT = "super fast pink and gold jet is flying against a beautiful nebula, trail behind it"
const STORAGE_KEY = "replicate_api_token";

export default function Home() {
const [texts, setTexts] = useState<string[]>([DEFAULT_INPUT]);
const [apiToken, setApiToken] = useState<string | null>(null);
const [tokenInput, setTokenInput] = useState("");
const [isLoading, setIsLoading] = useState(true);

// Load token from localStorage on mount
useEffect(() => {
const storedToken = localStorage.getItem(STORAGE_KEY);
if (storedToken) {
setApiToken(storedToken);
}
setIsLoading(false);
}, []);

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTexts([...texts, e.target.value]);
};

// Focus the input element when the page loads)
const handleTokenSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (tokenInput.trim()) {
localStorage.setItem(STORAGE_KEY, tokenInput.trim());
setApiToken(tokenInput.trim());
setTokenInput("");
}
};

const handleClearToken = () => {
localStorage.removeItem(STORAGE_KEY);
setApiToken(null);
setTexts([DEFAULT_INPUT]);
};

// Focus the input element when the page loads
const inputElement = useRef<HTMLInputElement>(null);
useEffect(() => {
if (inputElement.current) {
if (inputElement.current && apiToken) {
inputElement.current.focus();
}
}, []);
}, [apiToken]);

if (isLoading) {
return (
<div className="min-h-screen bg-stone-900 flex items-center justify-center">
<div className="text-stone-400">Loading...</div>
</div>
);
}

if (!apiToken) {
return (
<div className="min-h-screen bg-stone-900 flex items-center justify-center p-4">
<div className="max-w-md w-full bg-stone-800 rounded-lg shadow-xl p-8">
<h1 className="text-2xl font-bold text-stone-100 mb-4">Welcome to Fast Flux Demo</h1>
<p className="text-stone-300 mb-6">
Create a{" "}
<Link
href="https://replicate.com/account/api-tokens?new-token-name=fast-flux-demo"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
Replicate API token
</Link>
{" "}then paste it here:
</p>

<form onSubmit={handleTokenSubmit} className="space-y-4">
<div>
<label htmlFor="token" className="block text-sm font-medium text-stone-300 mb-2">
Replicate API Token
</label>
<input
id="token"
type="password"
value={tokenInput}
onChange={(e) => setTokenInput(e.target.value)}
placeholder="r8_..."
className="w-full p-3 bg-stone-700 text-stone-100 placeholder-stone-400 rounded border border-stone-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
/>
</div>

<button
type="submit"
disabled={!tokenInput.trim()}
className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-stone-600 disabled:cursor-not-allowed text-white font-medium py-3 px-4 rounded transition-colors"
>
Let&apos;s make some images
</button>
</form>

<div className="mt-6 pt-6 border-t border-stone-700">
<p className="text-sm text-stone-400">
Check out the GitHub repo at{" "}
<Link
href="https://github.com/replicate/fast-flux-demo"
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 hover:text-blue-300 underline"
>
replicate/fast-flux-demo
</Link>
</p>
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-stone-900 flex flex-col">
Expand All @@ -33,17 +130,26 @@ export default function Home() {
ref={inputElement}
onFocus={(e) => e.target.setSelectionRange(0, e.target.value.length)}
/>
<Link href="https://github.com/replicate/fast-flux-demo" target="_blank" rel="noopener noreferrer">
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" className="fill-white">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
</svg>
</Link>
<div className="flex items-center gap-4">
<button
onClick={handleClearToken}
className="text-stone-400 hover:text-stone-200 text-sm"
title="Clear API token"
>
Clear Token
</button>
<Link href="https://github.com/replicate/fast-flux-demo" target="_blank" rel="noopener noreferrer">
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" className="fill-white">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
</svg>
</Link>
</div>
</header>
<main>
{texts.map((text, index) => (
<Image
key={index}
src={`/api/generate-image?text=${encodeURIComponent(text)}`}
src={`/api/generate-image?text=${encodeURIComponent(text)}&token=${encodeURIComponent(apiToken)}`}
alt={text}
width={512}
height={512}
Expand Down