Repository: Nutlope/roomGPT
Branch: main
Commit: 611398c78da6
Files: 27
Total size: 37.8 KB
Directory structure:
gitextract_z9xg4qej/
├── .example.env
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── dream/
│ │ └── page.tsx
│ ├── generate/
│ │ └── route.ts
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── CompareSlider.tsx
│ ├── DropDown.tsx
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── LoadingDots.tsx
│ ├── ResizablePanel.tsx
│ ├── SquigglyLines.tsx
│ └── Toggle.tsx
├── next.config.js
├── package.json
├── postcss.config.js
├── styles/
│ ├── globals.css
│ └── loading-dots.module.css
├── tailwind.config.js
├── tsconfig.json
└── utils/
├── appendNewToName.ts
├── downloadPhoto.ts
├── dropdownTypes.ts
└── redis.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .example.env
================================================
REPLICATE_API_KEY=
NEXT_PUBLIC_UPLOAD_API_KEY=
# Optional, if you're doing rate limiting
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
/.vscode
# typescript
*.tsbuildinfo
next-env.d.ts
.env
================================================
FILE: LICENSE
================================================
MIT License Copyright (c) 2023 Hassan El Mghari
Permission is hereby granted, free of
charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice
(including the next paragraph) shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# [RoomGPT](https://roomGPT.io) - redesign your room with AI
This is the previous and open source version of RoomGPT.io (a paid SaaS product). It's the very first version of roomGPT without the auth, payments, or additional features and it's simple to clone, deploy, and play around with.
[](https://vercel.com/new/clone?repository-url=https://github.com/Nutlope/roomGPT&env=REPLICATE_API_KEY&project-name=room-GPT&repo-name=roomGPT)
[](https://roomGPT.io)
## How it works
It uses an ML model called [ControlNet](https://github.com/lllyasviel/ControlNet) to generate variations of rooms. This application gives you the ability to upload a photo of any room, which will send it through this ML Model using a Next.js API route, and return your generated room. The ML Model is hosted on [Replicate](https://replicate.com) and [Bytescale](https://www.bytescale.com/) is used for image storage.
## Running Locally
### Cloning the repository the local machine.
```bash
git clone https://github.com/Nutlope/roomGPT
```
### Creating a account on Replicate to get an API key.
1. Go to [Replicate](https://replicate.com/) to make an account.
2. Click on your profile picture in the top left corner, and click on "API Tokens".
3. Here you can find your API token. Copy it.
### Storing the API keys in .env
Create a file in root directory of project with env. And store your API key in it, as shown in the .example.env file.
If you'd also like to do rate limiting, create an account on UpStash, create a Redis database, and populate the two environment variables in `.env` as well. If you don't want to do rate limiting, you don't need to make any changes.
### Installing the dependencies.
```bash
npm install
```
### Running the application.
Then, run the application in the command line and it will be available at `http://localhost:3000`.
```bash
npm run dev
```
## One-Click Deploy
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):
[](https://vercel.com/new/clone?repository-url=https://github.com/Nutlope/roomGPT&env=REPLICATE_API_KEY&project-name=room-GPT&repo-name=roomGPT)
## License
This repo is MIT licensed.
================================================
FILE: app/dream/page.tsx
================================================
"use client";
import { AnimatePresence, motion } from "framer-motion";
import Image from "next/image";
import { useState } from "react";
import { UrlBuilder } from "@bytescale/sdk";
import { UploadWidgetConfig } from "@bytescale/upload-widget";
import { UploadDropzone } from "@bytescale/upload-widget-react";
import { CompareSlider } from "../../components/CompareSlider";
import Footer from "../../components/Footer";
import Header from "../../components/Header";
import LoadingDots from "../../components/LoadingDots";
import ResizablePanel from "../../components/ResizablePanel";
import Toggle from "../../components/Toggle";
import appendNewToName from "../../utils/appendNewToName";
import downloadPhoto from "../../utils/downloadPhoto";
import DropDown from "../../components/DropDown";
import { roomType, rooms, themeType, themes } from "../../utils/dropdownTypes";
const options: UploadWidgetConfig = {
apiKey: !!process.env.NEXT_PUBLIC_UPLOAD_API_KEY
? process.env.NEXT_PUBLIC_UPLOAD_API_KEY
: "free",
maxFileCount: 1,
mimeTypes: ["image/jpeg", "image/png", "image/jpg"],
editor: { images: { crop: false } },
styles: {
colors: {
primary: "#2563EB", // Primary buttons & links
error: "#d23f4d", // Error messages
shade100: "#fff", // Standard text
shade200: "#fffe", // Secondary button text
shade300: "#fffd", // Secondary button text (hover)
shade400: "#fffc", // Welcome text
shade500: "#fff9", // Modal close button
shade600: "#fff7", // Border
shade700: "#fff2", // Progress indicator background
shade800: "#fff1", // File item background
shade900: "#ffff", // Various (draggable crop buttons, etc.)
},
},
};
export default function DreamPage() {
const [originalPhoto, setOriginalPhoto] = useState<string | null>(null);
const [restoredImage, setRestoredImage] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(false);
const [restoredLoaded, setRestoredLoaded] = useState<boolean>(false);
const [sideBySide, setSideBySide] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const [photoName, setPhotoName] = useState<string | null>(null);
const [theme, setTheme] = useState<themeType>("Modern");
const [room, setRoom] = useState<roomType>("Living Room");
const UploadDropZone = () => (
<UploadDropzone
options={options}
onUpdate={({ uploadedFiles }) => {
if (uploadedFiles.length !== 0) {
const image = uploadedFiles[0];
const imageName = image.originalFile.originalFileName;
const imageUrl = UrlBuilder.url({
accountId: image.accountId,
filePath: image.filePath,
options: {
transformation: "preset",
transformationPreset: "thumbnail"
}
});
setPhotoName(imageName);
setOriginalPhoto(imageUrl);
generatePhoto(imageUrl);
}
}}
width="670px"
height="250px"
/>
);
async function generatePhoto(fileUrl: string) {
await new Promise((resolve) => setTimeout(resolve, 200));
setLoading(true);
const res = await fetch("/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ imageUrl: fileUrl, theme, room }),
});
let newPhoto = await res.json();
if (res.status !== 200) {
setError(newPhoto);
} else {
setRestoredImage(newPhoto[1]);
}
setTimeout(() => {
setLoading(false);
}, 1300);
}
return (
<div className="flex max-w-6xl mx-auto flex-col items-center justify-center py-2 min-h-screen">
<Header />
<main className="flex flex-1 w-full flex-col items-center justify-center text-center px-4 mt-4 sm:mb-0 mb-8">
<h1 className="mx-auto max-w-4xl font-display text-4xl font-bold tracking-normal text-slate-100 sm:text-6xl mb-5">
Generate your <span className="text-blue-600">dream</span> room
</h1>
<ResizablePanel>
<AnimatePresence mode="wait">
<motion.div className="flex justify-between items-center w-full flex-col mt-4">
{!restoredImage && (
<>
<div className="space-y-4 w-full max-w-sm">
<div className="flex mt-3 items-center space-x-3">
<Image
src="/number-1-white.svg"
width={30}
height={30}
alt="1 icon"
/>
<p className="text-left font-medium">
Choose your room theme.
</p>
</div>
<DropDown
theme={theme}
setTheme={(newTheme) =>
setTheme(newTheme as typeof theme)
}
themes={themes}
/>
</div>
<div className="space-y-4 w-full max-w-sm">
<div className="flex mt-10 items-center space-x-3">
<Image
src="/number-2-white.svg"
width={30}
height={30}
alt="1 icon"
/>
<p className="text-left font-medium">
Choose your room type.
</p>
</div>
<DropDown
theme={room}
setTheme={(newRoom) => setRoom(newRoom as typeof room)}
themes={rooms}
/>
</div>
<div className="mt-4 w-full max-w-sm">
<div className="flex mt-6 w-96 items-center space-x-3">
<Image
src="/number-3-white.svg"
width={30}
height={30}
alt="1 icon"
/>
<p className="text-left font-medium">
Upload a picture of your room.
</p>
</div>
</div>
</>
)}
{restoredImage && (
<div>
Here's your remodeled <b>{room.toLowerCase()}</b> in the{" "}
<b>{theme.toLowerCase()}</b> theme!{" "}
</div>
)}
<div
className={`${
restoredLoaded ? "visible mt-6 -ml-8" : "invisible"
}`}
>
<Toggle
className={`${restoredLoaded ? "visible mb-6" : "invisible"}`}
sideBySide={sideBySide}
setSideBySide={(newVal) => setSideBySide(newVal)}
/>
</div>
{restoredLoaded && sideBySide && (
<CompareSlider
original={originalPhoto!}
restored={restoredImage!}
/>
)}
{!originalPhoto && <UploadDropZone />}
{originalPhoto && !restoredImage && (
<Image
alt="original photo"
src={originalPhoto}
className="rounded-2xl h-96"
width={475}
height={475}
/>
)}
{restoredImage && originalPhoto && !sideBySide && (
<div className="flex sm:space-x-4 sm:flex-row flex-col">
<div>
<h2 className="mb-1 font-medium text-lg">Original Room</h2>
<Image
alt="original photo"
src={originalPhoto}
className="rounded-2xl relative w-full h-96"
width={475}
height={475}
/>
</div>
<div className="sm:mt-0 mt-8">
<h2 className="mb-1 font-medium text-lg">Generated Room</h2>
<a href={restoredImage} target="_blank" rel="noreferrer">
<Image
alt="restored photo"
src={restoredImage}
className="rounded-2xl relative sm:mt-0 mt-2 cursor-zoom-in w-full h-96"
width={475}
height={475}
onLoadingComplete={() => setRestoredLoaded(true)}
/>
</a>
</div>
</div>
)}
{loading && (
<button
disabled
className="bg-blue-500 rounded-full text-white font-medium px-4 pt-2 pb-3 mt-8 w-40"
>
<span className="pt-4">
<LoadingDots color="white" style="large" />
</span>
</button>
)}
{error && (
<div
className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-xl mt-8"
role="alert"
>
<span className="block sm:inline">{error}</span>
</div>
)}
<div className="flex space-x-2 justify-center">
{originalPhoto && !loading && (
<button
onClick={() => {
setOriginalPhoto(null);
setRestoredImage(null);
setRestoredLoaded(false);
setError(null);
}}
className="bg-blue-500 rounded-full text-white font-medium px-4 py-2 mt-8 hover:bg-blue-500/80 transition"
>
Generate New Room
</button>
)}
{restoredLoaded && (
<button
onClick={() => {
downloadPhoto(
restoredImage!,
appendNewToName(photoName!)
);
}}
className="bg-white rounded-full text-black border font-medium px-4 py-2 mt-8 hover:bg-gray-100 transition"
>
Download Generated Room
</button>
)}
</div>
</motion.div>
</AnimatePresence>
</ResizablePanel>
</main>
<Footer />
</div>
);
}
================================================
FILE: app/generate/route.ts
================================================
import { Ratelimit } from "@upstash/ratelimit";
import redis from "../../utils/redis";
import { NextResponse } from "next/server";
import { headers } from "next/headers";
// Create a new ratelimiter, that allows 5 requests per 24 hours
const ratelimit = redis
? new Ratelimit({
redis: redis,
limiter: Ratelimit.fixedWindow(5, "1440 m"),
analytics: true,
})
: undefined;
export async function POST(request: Request) {
// Rate Limiter Code
if (ratelimit) {
const headersList = headers();
const ipIdentifier = headersList.get("x-real-ip");
const result = await ratelimit.limit(ipIdentifier ?? "");
if (!result.success) {
return new Response(
"Too many uploads in 1 day. Please try again in a 24 hours.",
{
status: 429,
headers: {
"X-RateLimit-Limit": result.limit,
"X-RateLimit-Remaining": result.remaining,
} as any,
}
);
}
}
const { imageUrl, theme, room } = await request.json();
// POST request to Replicate to start the image restoration generation process
let startResponse = await fetch("https://api.replicate.com/v1/predictions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Token " + process.env.REPLICATE_API_KEY,
},
body: JSON.stringify({
version:
"854e8727697a057c525cdb45ab037f64ecca770a1769cc52287c2e56472a247b",
input: {
image: imageUrl,
prompt:
room === "Gaming Room"
? "a room for gaming with gaming computers, gaming consoles, and gaming chairs"
: `a ${theme.toLowerCase()} ${room.toLowerCase()}`,
a_prompt:
"best quality, extremely detailed, photo from Pinterest, interior, cinematic photo, ultra-detailed, ultra-realistic, award-winning",
n_prompt:
"longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality",
},
}),
});
let jsonStartResponse = await startResponse.json();
let endpointUrl = jsonStartResponse.urls.get;
// GET request to get the status of the image restoration process & return the result when it's ready
let restoredImage: string | null = null;
while (!restoredImage) {
// Loop in 1s intervals until the alt text is ready
console.log("polling for result...");
let finalResponse = await fetch(endpointUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Token " + process.env.REPLICATE_API_KEY,
},
});
let jsonFinalResponse = await finalResponse.json();
if (jsonFinalResponse.status === "succeeded") {
restoredImage = jsonFinalResponse.output;
} else if (jsonFinalResponse.status === "failed") {
break;
} else {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
return NextResponse.json(
restoredImage ? restoredImage : "Failed to restore image"
);
}
================================================
FILE: app/layout.tsx
================================================
import { Analytics } from "@vercel/analytics/react";
import { Metadata } from "next";
import "../styles/globals.css";
let title = "Dream Room Generator";
let description = "Generate your dream room in seconds.";
let ogimage = "https://roomgpt-demo.vercel.app/og-image.png";
let sitename = "roomGPT.io";
export const metadata: Metadata = {
title,
description,
icons: {
icon: "/favicon.ico",
},
openGraph: {
images: [ogimage],
title,
description,
url: "https://roomgpt-demo.vercel.app",
siteName: sitename,
locale: "en_US",
type: "website",
},
twitter: {
card: "summary_large_image",
images: [ogimage],
title,
description,
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="bg-[#17181C] text-white">
{children}
<Analytics />
</body>
</html>
);
}
================================================
FILE: app/page.tsx
================================================
import Image from "next/image";
import Link from "next/link";
import Footer from "../components/Footer";
import Header from "../components/Header";
import SquigglyLines from "../components/SquigglyLines";
export default function HomePage() {
return (
<div className="flex max-w-6xl mx-auto flex-col items-center justify-center py-2 min-h-screen">
<Header />
<main className="flex flex-1 w-full flex-col items-center justify-center text-center px-4 sm:mt-20 mt-20 background-gradient">
<a
href="https://vercel.fyi/roomGPT"
target="_blank"
rel="noreferrer"
className="border border-gray-700 rounded-lg py-2 px-4 text-gray-400 text-sm mb-5 transition duration-300 ease-in-out"
>
Clone and deploy your own with{" "}
<span className="text-blue-600">Vercel</span>
</a>
<h1 className="mx-auto max-w-4xl font-display text-5xl font-bold tracking-normal text-gray-300 sm:text-7xl">
Generating dream rooms{" "}
<span className="relative whitespace-nowrap text-blue-600">
<SquigglyLines />
<span className="relative">using AI</span>
</span>{" "}
for everyone.
</h1>
<h2 className="mx-auto mt-12 max-w-xl text-lg sm:text-gray-400 text-gray-500 leading-7">
Take a picture of your room and see how your room looks in different
themes. 100% free – remodel your room today.
</h2>
<Link
className="bg-blue-600 rounded-xl text-white font-medium px-4 py-3 sm:mt-10 mt-8 hover:bg-blue-500 transition"
href="/dream"
>
Generate your dream room
</Link>
<div className="flex justify-between items-center w-full flex-col sm:mt-10 mt-6">
<div className="flex flex-col space-y-10 mt-4 mb-16">
<div className="flex sm:space-x-8 sm:flex-row flex-col">
<div>
<h3 className="mb-1 font-medium text-lg">Original Room</h3>
<Image
alt="Original photo of a room with roomGPT.io"
src="/original-pic.jpg"
className="w-full object-cover h-96 rounded-2xl"
width={400}
height={400}
/>
</div>
<div className="sm:mt-0 mt-8">
<h3 className="mb-1 font-medium text-lg">Generated Room</h3>
<Image
alt="Generated photo of a room with roomGPT.io"
width={400}
height={400}
src="/generated-pic-2.jpg"
className="w-full object-cover h-96 rounded-2xl sm:mt-0 mt-2"
/>
</div>
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
================================================
FILE: components/CompareSlider.tsx
================================================
import {
ReactCompareSlider,
ReactCompareSliderImage,
} from "react-compare-slider";
export const CompareSlider = ({
original,
restored,
}: {
original: string;
restored: string;
}) => {
return (
<ReactCompareSlider
itemOne={<ReactCompareSliderImage src={original} alt="original photo" />}
itemTwo={<ReactCompareSliderImage src={restored} alt="generated photo" />}
portrait
className="flex w-[600px] mt-5 h-96"
/>
);
};
================================================
FILE: components/DropDown.tsx
================================================
import { Menu, Transition } from "@headlessui/react";
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from "@heroicons/react/20/solid";
import { Fragment } from "react";
import { roomType, themeType } from "../utils/dropdownTypes";
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
interface DropDownProps {
theme: themeType | roomType;
setTheme: (theme: themeType | roomType) => void;
themes: themeType[] | roomType[];
}
// TODO: Change names since this is a generic dropdown now
export default function DropDown({ theme, setTheme, themes }: DropDownProps) {
return (
<Menu as="div" className="relative block text-left">
<div>
<Menu.Button className="inline-flex w-full justify-between items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-black">
{theme}
<ChevronUpIcon
className="-mr-1 ml-2 h-5 w-5 ui-open:hidden"
aria-hidden="true"
/>
<ChevronDownIcon
className="-mr-1 ml-2 h-5 w-5 hidden ui-open:block"
aria-hidden="true"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items
className="absolute left-0 z-10 mt-2 w-full origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none overflow-hidden"
key={theme}
>
<div className="">
{themes.map((themeItem) => (
<Menu.Item key={themeItem}>
{({ active }) => (
<button
onClick={() => setTheme(themeItem)}
className={classNames(
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
themeItem === theme ? "bg-gray-200" : "",
"px-4 py-2 text-sm w-full text-left flex items-center space-x-2 justify-between"
)}
>
<span>{themeItem}</span>
{themeItem === theme ? (
<CheckIcon className="w-4 h-4 text-bold" />
) : null}
</button>
)}
</Menu.Item>
))}
</div>
</Menu.Items>
</Transition>
</Menu>
);
}
================================================
FILE: components/Footer.tsx
================================================
import Link from "next/link";
export default function Footer() {
return (
<footer className="text-center h-16 sm:h-20 w-full sm:pt-2 pt-4 border-t mt-5 flex sm:flex-row flex-col justify-between items-center px-3 space-y-3 sm:mb-0 mb-3 border-gray-500">
<div className="text-gray-500">
Powered by{" "}
<a
href="https://replicate.com/"
target="_blank"
rel="noreferrer"
className="font-bold hover:underline transition hover:text-gray-300 underline-offset-2"
>
Replicate,{" "}
</a>
<a
href="https://www.bytescale.com/"
target="_blank"
rel="noreferrer"
className="font-bold hover:underline hover:text-gray-300 transition underline-offset-2"
>
Bytescale,{" "}
</a>
and{" "}
<a
href="https://vercel.com/"
target="_blank"
rel="noreferrer"
className="font-bold hover:underline transition hover:text-gray-300 underline-offset-2"
>
Vercel.
</a>
</div>
<div className="flex space-x-4 pb-4 sm:pb-0">
<Link
href="https://twitter.com/nutlope"
className="group"
aria-label="TaxPal on Twitter"
>
<svg
aria-hidden="true"
className="h-6 w-6 fill-gray-500 group-hover:fill-gray-300"
>
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0 0 22 5.92a8.19 8.19 0 0 1-2.357.646 4.118 4.118 0 0 0 1.804-2.27 8.224 8.224 0 0 1-2.605.996 4.107 4.107 0 0 0-6.993 3.743 11.65 11.65 0 0 1-8.457-4.287 4.106 4.106 0 0 0 1.27 5.477A4.073 4.073 0 0 1 2.8 9.713v.052a4.105 4.105 0 0 0 3.292 4.022 4.093 4.093 0 0 1-1.853.07 4.108 4.108 0 0 0 3.834 2.85A8.233 8.233 0 0 1 2 18.407a11.615 11.615 0 0 0 6.29 1.84" />
</svg>
</Link>
<Link
href="https://github.com/Nutlope/roomGPT"
className="group"
aria-label="TaxPal on GitHub"
>
<svg
aria-hidden="true"
className="h-6 w-6 fill-gray-500 group-hover:fill-gray-300"
>
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
</svg>
</Link>
</div>
</footer>
);
}
================================================
FILE: components/Header.tsx
================================================
import Image from "next/image";
import Link from "next/link";
export default function Header() {
return (
<header className="flex flex-col xs:flex-row justify-between items-center w-full mt-3 border-b pb-7 sm:px-4 px-2 border-gray-500 gap-2">
<Link href="/" className="flex space-x-2">
<Image
alt="header text"
src="/bed.svg"
className="sm:w-10 sm:h-10 w-9 h-9"
width={24}
height={24}
/>
<h1 className="sm:text-3xl text-xl font-bold ml-2 tracking-tight">
roomGPT.io
</h1>
</Link>
<a
className="flex max-w-fit items-center justify-center space-x-2 rounded-full border border-blue-600 text-white px-5 py-2 text-sm shadow-md hover:bg-blue-500 bg-blue-600 font-medium transition"
href="https://github.com/Nutlope/roomGPT"
target="_blank"
rel="noopener noreferrer"
>
<Github />
<p>Star on GitHub</p>
</a>
</header>
);
}
function Github({ className }: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
fill="currentColor"
viewBox="0 0 24 24"
className={className}
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
</svg>
);
}
================================================
FILE: components/LoadingDots.tsx
================================================
import styles from "../styles/loading-dots.module.css";
const LoadingDots = ({
color = "#000",
style = "small",
}: {
color: string;
style: string;
}) => {
return (
<span className={style == "small" ? styles.loading2 : styles.loading}>
<span style={{ backgroundColor: color }} />
<span style={{ backgroundColor: color }} />
<span style={{ backgroundColor: color }} />
</span>
);
};
export default LoadingDots;
LoadingDots.defaultProps = {
style: "small",
};
================================================
FILE: components/ResizablePanel.tsx
================================================
import { motion } from "framer-motion";
import useMeasure from "react-use-measure";
export default function ResizablePanel({
children,
}: {
children: React.ReactNode;
}) {
let [ref, { height }] = useMeasure();
return (
<motion.div
animate={height ? { height } : {}}
style={height ? { height } : {}}
className="relative w-full overflow-hidden"
transition={{ type: "tween", duration: 0.5 }}
>
<div ref={ref} className={height ? "absolute inset-x-0" : "relative"}>
{children}
</div>
</motion.div>
);
}
================================================
FILE: components/SquigglyLines.tsx
================================================
export default function SquigglyLines() {
return (
<svg
aria-hidden="true"
viewBox="0 0 418 42"
className="absolute top-2/3 left-0 h-[0.58em] w-full fill-blue-500/60"
preserveAspectRatio="none"
>
<path d="M203.371.916c-26.013-2.078-76.686 1.963-124.73 9.946L67.3 12.749C35.421 18.062 18.2 21.766 6.004 25.934 1.244 27.561.828 27.778.874 28.61c.07 1.214.828 1.121 9.595-1.176 9.072-2.377 17.15-3.92 39.246-7.496C123.565 7.986 157.869 4.492 195.942 5.046c7.461.108 19.25 1.696 19.17 2.582-.107 1.183-7.874 4.31-25.75 10.366-21.992 7.45-35.43 12.534-36.701 13.884-2.173 2.308-.202 4.407 4.442 4.734 2.654.187 3.263.157 15.593-.78 35.401-2.686 57.944-3.488 88.365-3.143 46.327.526 75.721 2.23 130.788 7.584 19.787 1.924 20.814 1.98 24.557 1.332l.066-.011c1.201-.203 1.53-1.825.399-2.335-2.911-1.31-4.893-1.604-22.048-3.261-57.509-5.556-87.871-7.36-132.059-7.842-23.239-.254-33.617-.116-50.627.674-11.629.54-42.371 2.494-46.696 2.967-2.359.259 8.133-3.625 26.504-9.81 23.239-7.825 27.934-10.149 28.304-14.005.417-4.348-3.529-6-16.878-7.066Z" />
</svg>
);
}
================================================
FILE: components/Toggle.tsx
================================================
import { Switch } from "@headlessui/react";
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
export interface ToggleProps extends React.HTMLAttributes<HTMLDivElement> {
sideBySide: boolean;
setSideBySide: (sideBySide: boolean) => void;
}
export default function Toggle({
sideBySide,
setSideBySide,
...props
}: ToggleProps) {
return (
<Switch.Group as="div" {...props}>
<div className="flex items-center">
<span
className={`text-sm mr-3 font-medium ${
!sideBySide ? "text-white" : "text-gray-500"
}`}
>
Side by Side
</span>
<Switch
checked={sideBySide}
onChange={setSideBySide}
className={classNames(
sideBySide ? "bg-blue-600" : "bg-gray-200",
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none "
)}
>
<span
aria-hidden="true"
className={classNames(
sideBySide ? "translate-x-5" : "translate-x-0",
"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
)}
/>
</Switch>
<Switch.Label as="span" className="ml-3">
<span
className={`text-sm font-medium ${
sideBySide ? "text-white" : "text-gray-500"
} `}
>
Compare
</span>
</Switch.Label>
</div>
</Switch.Group>
);
}
================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
images: {
domains: ["upcdn.io", "replicate.delivery"],
},
};
================================================
FILE: package.json
================================================
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"@bytescale/upload-widget-react": "^4.9.0",
"@headlessui/react": "^1.7.7",
"@headlessui/tailwindcss": "^0.1.2",
"@heroicons/react": "^2.0.16",
"@tailwindcss/forms": "^0.5.3",
"@upstash/ratelimit": "^0.3.8",
"@upstash/redis": "^1.19.1",
"@vercel/analytics": "^0.1.11",
"framer-motion": "^8.2.4",
"next": "^13.4.4",
"react": "^18.2.0",
"react-compare-slider": "^2.2.0",
"react-countup": "^6.4.0",
"react-dom": "^18.2.0",
"react-use-measure": "^2.1.1",
"request-ip": "^3.3.0"
},
"devDependencies": {
"@types/node": "18.11.3",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",
"@types/request-ip": "^0.0.37",
"autoprefixer": "^10.4.12",
"postcss": "^8.4.18",
"tailwindcss": "^3.2.4",
"typescript": "4.9.4"
}
}
================================================
FILE: postcss.config.js
================================================
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
================================================
FILE: styles/globals.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;
@media (min-width: 400px) {
.background-gradient::before {
background: radial-gradient(
20% 50% at 50% 50%,
rgba(71, 127, 247, 0.376) 0%,
rgba(37, 38, 44, 0) 100%
);
z-index: -10;
content: "";
position: absolute;
inset: 0px;
transform: scale(1);
pointer-events: none;
}
}
================================================
FILE: styles/loading-dots.module.css
================================================
.loading {
display: inline-flex;
align-items: center;
}
.loading .spacer {
margin-right: 2px;
}
.loading span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: 5px;
height: 5px;
border-radius: 50%;
display: inline-block;
margin: 0 1px;
}
.loading span:nth-of-type(2) {
animation-delay: 0.2s;
}
.loading span:nth-of-type(3) {
animation-delay: 0.4s;
}
.loading2 {
display: inline-flex;
align-items: center;
}
.loading2 .spacer {
margin-right: 2px;
}
.loading2 span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: 4px;
height: 4px;
border-radius: 50%;
display: inline-block;
margin: 0 1px;
}
.loading2 span:nth-of-type(2) {
animation-delay: 0.2s;
}
.loading2 span:nth-of-type(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
================================================
FILE: tailwind.config.js
================================================
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
"./app/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
screens: {
xs: "330px",
},
},
},
plugins: [require("@tailwindcss/forms"), require("@headlessui/tailwindcss")],
};
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
================================================
FILE: utils/appendNewToName.ts
================================================
export default function appendNewToName(name: string) {
let insertPos = name.indexOf(".");
let newName = name
.substring(0, insertPos)
.concat("-new", name.substring(insertPos));
return newName;
}
================================================
FILE: utils/downloadPhoto.ts
================================================
function forceDownload(blobUrl: string, filename: string) {
let a: any = document.createElement("a");
a.download = filename;
a.href = blobUrl;
document.body.appendChild(a);
a.click();
a.remove();
}
export default function downloadPhoto(url: string, filename: string) {
fetch(url, {
headers: new Headers({
Origin: location.origin,
}),
mode: "cors",
})
.then((response) => response.blob())
.then((blob) => {
let blobUrl = window.URL.createObjectURL(blob);
forceDownload(blobUrl, filename);
})
.catch((e) => console.error(e));
}
================================================
FILE: utils/dropdownTypes.ts
================================================
export type themeType =
| "Modern"
| "Vintage"
| "Minimalist"
| "Professional"
| "Tropical";
export type roomType =
| "Living Room"
| "Dining Room"
| "Bedroom"
| "Bathroom"
| "Office"
| "Gaming Room";
export const themes: themeType[] = [
"Modern",
"Minimalist",
"Professional",
"Tropical",
"Vintage",
];
export const rooms: roomType[] = [
"Living Room",
"Dining Room",
"Office",
"Bedroom",
"Bathroom",
"Gaming Room",
];
================================================
FILE: utils/redis.ts
================================================
import { Redis } from "@upstash/redis";
const redis =
!!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_REDIS_REST_TOKEN
? new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
: undefined;
export default redis;
gitextract_z9xg4qej/
├── .example.env
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── dream/
│ │ └── page.tsx
│ ├── generate/
│ │ └── route.ts
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── CompareSlider.tsx
│ ├── DropDown.tsx
│ ├── Footer.tsx
│ ├── Header.tsx
│ ├── LoadingDots.tsx
│ ├── ResizablePanel.tsx
│ ├── SquigglyLines.tsx
│ └── Toggle.tsx
├── next.config.js
├── package.json
├── postcss.config.js
├── styles/
│ ├── globals.css
│ └── loading-dots.module.css
├── tailwind.config.js
├── tsconfig.json
└── utils/
├── appendNewToName.ts
├── downloadPhoto.ts
├── dropdownTypes.ts
└── redis.ts
SYMBOL INDEX (20 symbols across 13 files)
FILE: app/dream/page.tsx
function DreamPage (line 44) | function DreamPage() {
FILE: app/generate/route.ts
function POST (line 15) | async function POST(request: Request) {
FILE: app/layout.tsx
function RootLayout (line 33) | function RootLayout({
FILE: app/page.tsx
function HomePage (line 7) | function HomePage() {
FILE: components/DropDown.tsx
function classNames (line 10) | function classNames(...classes: string[]) {
type DropDownProps (line 14) | interface DropDownProps {
function DropDown (line 21) | function DropDown({ theme, setTheme, themes }: DropDownProps) {
FILE: components/Footer.tsx
function Footer (line 3) | function Footer() {
FILE: components/Header.tsx
function Header (line 4) | function Header() {
function Github (line 32) | function Github({ className }: { className?: string }) {
FILE: components/ResizablePanel.tsx
function ResizablePanel (line 4) | function ResizablePanel({
FILE: components/SquigglyLines.tsx
function SquigglyLines (line 1) | function SquigglyLines() {
FILE: components/Toggle.tsx
function classNames (line 3) | function classNames(...classes: string[]) {
type ToggleProps (line 7) | interface ToggleProps extends React.HTMLAttributes<HTMLDivElement> {
function Toggle (line 12) | function Toggle({
FILE: utils/appendNewToName.ts
function appendNewToName (line 1) | function appendNewToName(name: string) {
FILE: utils/downloadPhoto.ts
function forceDownload (line 1) | function forceDownload(blobUrl: string, filename: string) {
function downloadPhoto (line 10) | function downloadPhoto(url: string, filename: string) {
FILE: utils/dropdownTypes.ts
type themeType (line 1) | type themeType =
type roomType (line 8) | type roomType =
Condensed preview — 27 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (42K chars).
[
{
"path": ".example.env",
"chars": 140,
"preview": "REPLICATE_API_KEY=\nNEXT_PUBLIC_UPLOAD_API_KEY=\n\n# Optional, if you're doing rate limiting\nUPSTASH_REDIS_REST_URL=\nUPSTAS"
},
{
"path": ".gitignore",
"chars": 399,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "LICENSE",
"chars": 1103,
"preview": "MIT License Copyright (c) 2023 Hassan El Mghari\n\nPermission is hereby granted, free of\ncharge, to any person obtaining a"
},
{
"path": "README.md",
"chars": 2332,
"preview": "# [RoomGPT](https://roomGPT.io) - redesign your room with AI\n\nThis is the previous and open source version of RoomGPT.io"
},
{
"path": "app/dream/page.tsx",
"chars": 10761,
"preview": "\"use client\";\n\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport Image from \"next/image\";\nimport { useStat"
},
{
"path": "app/generate/route.ts",
"chars": 3042,
"preview": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport redis from \"../../utils/redis\";\nimport { NextResponse } from \"nex"
},
{
"path": "app/layout.tsx",
"chars": 937,
"preview": "import { Analytics } from \"@vercel/analytics/react\";\nimport { Metadata } from \"next\";\nimport \"../styles/globals.css\";\n\nl"
},
{
"path": "app/page.tsx",
"chars": 2857,
"preview": "import Image from \"next/image\";\nimport Link from \"next/link\";\nimport Footer from \"../components/Footer\";\nimport Header f"
},
{
"path": "components/CompareSlider.tsx",
"chars": 468,
"preview": "import {\n ReactCompareSlider,\n ReactCompareSliderImage,\n} from \"react-compare-slider\";\n\nexport const CompareSlider = ("
},
{
"path": "components/DropDown.tsx",
"chars": 2721,
"preview": "import { Menu, Transition } from \"@headlessui/react\";\nimport {\n CheckIcon,\n ChevronDownIcon,\n ChevronUpIcon,\n} from \""
},
{
"path": "components/Footer.tsx",
"chars": 3008,
"preview": "import Link from \"next/link\";\n\nexport default function Footer() {\n return (\n <footer className=\"text-center h-16 sm:"
},
{
"path": "components/Header.tsx",
"chars": 1989,
"preview": "import Image from \"next/image\";\nimport Link from \"next/link\";\n\nexport default function Header() {\n return (\n <header"
},
{
"path": "components/LoadingDots.tsx",
"chars": 500,
"preview": "import styles from \"../styles/loading-dots.module.css\";\n\nconst LoadingDots = ({\n color = \"#000\",\n style = \"small\",\n}: "
},
{
"path": "components/ResizablePanel.tsx",
"chars": 567,
"preview": "import { motion } from \"framer-motion\";\nimport useMeasure from \"react-use-measure\";\n\nexport default function ResizablePa"
},
{
"path": "components/SquigglyLines.tsx",
"chars": 1098,
"preview": "export default function SquigglyLines() {\n return (\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 418 42\"\n "
},
{
"path": "components/Toggle.tsx",
"chars": 1657,
"preview": "import { Switch } from \"@headlessui/react\";\n\nfunction classNames(...classes: string[]) {\n return classes.filter(Boolean"
},
{
"path": "next.config.js",
"chars": 154,
"preview": "/** @type {import('next').NextConfig} */\nmodule.exports = {\n reactStrictMode: true,\n images: {\n domains: [\"upcdn.io"
},
{
"path": "package.json",
"chars": 952,
"preview": "{\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev\",\n \"build\": \"next build\",\n \"start\": \"next start\"\n },\n \""
},
{
"path": "postcss.config.js",
"chars": 82,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n}\n"
},
{
"path": "styles/globals.css",
"chars": 386,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@media (min-width: 400px) {\n .background-gradient::before {"
},
{
"path": "styles/loading-dots.module.css",
"chars": 1024,
"preview": ".loading {\n display: inline-flex;\n align-items: center;\n}\n\n.loading .spacer {\n margin-right: 2px;\n}\n\n.loading span {\n"
},
{
"path": "tailwind.config.js",
"chars": 359,
"preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n content: [\n \"./pages/**/*.{js,ts,jsx,tsx}\",\n \"./c"
},
{
"path": "tsconfig.json",
"chars": 647,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\n \"dom\",\n \"dom.iterable\",\n \"esnext\"\n ],\n "
},
{
"path": "utils/appendNewToName.ts",
"chars": 211,
"preview": "export default function appendNewToName(name: string) {\n let insertPos = name.indexOf(\".\");\n let newName = name\n .s"
},
{
"path": "utils/downloadPhoto.ts",
"chars": 589,
"preview": "function forceDownload(blobUrl: string, filename: string) {\n let a: any = document.createElement(\"a\");\n a.download = f"
},
{
"path": "utils/dropdownTypes.ts",
"chars": 466,
"preview": "export type themeType =\n | \"Modern\"\n | \"Vintage\"\n | \"Minimalist\"\n | \"Professional\"\n | \"Tropical\";\n\nexport type room"
},
{
"path": "utils/redis.ts",
"chars": 305,
"preview": "import { Redis } from \"@upstash/redis\";\n\nconst redis =\n !!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_R"
}
]
About this extraction
This page contains the full source code of the Nutlope/roomGPT GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 27 files (37.8 KB), approximately 11.2k tokens, and a symbol index with 20 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.