[
  {
    "path": ".example.env",
    "content": "REPLICATE_API_KEY=\nNEXT_PUBLIC_UPLOAD_API_KEY=\n\n# Optional, if you're doing rate limiting\nUPSTASH_REDIS_REST_URL=\nUPSTASH_REDIS_REST_TOKEN=\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env*.local\n\n# vercel\n.vercel\n\n/.vscode\n# typescript\n*.tsbuildinfo\nnext-env.d.ts\n.env\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License Copyright (c) 2023 Hassan El Mghari\n\nPermission is hereby granted, free of\ncharge, to any person obtaining a copy of this software and associated\ndocumentation files (the \"Software\"), to deal in the Software without\nrestriction, including without limitation the rights to use, copy, modify, merge,\npublish, distribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to the\nfollowing conditions:\n\nThe above copyright notice and this permission notice\n(including the next paragraph) shall be included in all copies or substantial\nportions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF\nANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO\nEVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR\nOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\nFROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# [RoomGPT](https://roomGPT.io) - redesign your room with AI\n\nThis 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.\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Nutlope/roomGPT&env=REPLICATE_API_KEY&project-name=room-GPT&repo-name=roomGPT)\n\n[![Room GPT](./public/screenshot.png)](https://roomGPT.io)\n\n## How it works\n\nIt 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.\n\n## Running Locally\n\n### Cloning the repository the local machine.\n\n```bash\ngit clone https://github.com/Nutlope/roomGPT\n```\n\n### Creating a account on Replicate to get an API key.\n\n1. Go to [Replicate](https://replicate.com/) to make an account.\n2. Click on your profile picture in the top left corner, and click on \"API Tokens\".\n3. Here you can find your API token. Copy it.\n\n### Storing the API keys in .env\n\nCreate a file in root directory of project with env. And store your API key in it, as shown in the .example.env file.\n\nIf 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.\n\n### Installing the dependencies.\n\n```bash\nnpm install\n```\n\n### Running the application.\n\nThen, run the application in the command line and it will be available at `http://localhost:3000`.\n\n```bash\nnpm run dev\n```\n\n## One-Click Deploy\n\nDeploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=vercel-examples):\n\n[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/Nutlope/roomGPT&env=REPLICATE_API_KEY&project-name=room-GPT&repo-name=roomGPT)\n\n## License\n\nThis repo is MIT licensed.\n"
  },
  {
    "path": "app/dream/page.tsx",
    "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"framer-motion\";\nimport Image from \"next/image\";\nimport { useState } from \"react\";\nimport { UrlBuilder } from \"@bytescale/sdk\";\nimport { UploadWidgetConfig } from \"@bytescale/upload-widget\";\nimport { UploadDropzone } from \"@bytescale/upload-widget-react\";\nimport { CompareSlider } from \"../../components/CompareSlider\";\nimport Footer from \"../../components/Footer\";\nimport Header from \"../../components/Header\";\nimport LoadingDots from \"../../components/LoadingDots\";\nimport ResizablePanel from \"../../components/ResizablePanel\";\nimport Toggle from \"../../components/Toggle\";\nimport appendNewToName from \"../../utils/appendNewToName\";\nimport downloadPhoto from \"../../utils/downloadPhoto\";\nimport DropDown from \"../../components/DropDown\";\nimport { roomType, rooms, themeType, themes } from \"../../utils/dropdownTypes\";\n\nconst options: UploadWidgetConfig = {\n  apiKey: !!process.env.NEXT_PUBLIC_UPLOAD_API_KEY\n      ? process.env.NEXT_PUBLIC_UPLOAD_API_KEY\n      : \"free\",\n  maxFileCount: 1,\n  mimeTypes: [\"image/jpeg\", \"image/png\", \"image/jpg\"],\n  editor: { images: { crop: false } },\n  styles: {\n    colors: {\n      primary: \"#2563EB\", // Primary buttons & links\n      error: \"#d23f4d\", // Error messages\n      shade100: \"#fff\", // Standard text\n      shade200: \"#fffe\", // Secondary button text\n      shade300: \"#fffd\", // Secondary button text (hover)\n      shade400: \"#fffc\", // Welcome text\n      shade500: \"#fff9\", // Modal close button\n      shade600: \"#fff7\", // Border\n      shade700: \"#fff2\", // Progress indicator background\n      shade800: \"#fff1\", // File item background\n      shade900: \"#ffff\", // Various (draggable crop buttons, etc.)\n    },\n  },\n};\n\nexport default function DreamPage() {\n  const [originalPhoto, setOriginalPhoto] = useState<string | null>(null);\n  const [restoredImage, setRestoredImage] = useState<string | null>(null);\n  const [loading, setLoading] = useState<boolean>(false);\n  const [restoredLoaded, setRestoredLoaded] = useState<boolean>(false);\n  const [sideBySide, setSideBySide] = useState<boolean>(false);\n  const [error, setError] = useState<string | null>(null);\n  const [photoName, setPhotoName] = useState<string | null>(null);\n  const [theme, setTheme] = useState<themeType>(\"Modern\");\n  const [room, setRoom] = useState<roomType>(\"Living Room\");\n\n  const UploadDropZone = () => (\n    <UploadDropzone\n      options={options}\n      onUpdate={({ uploadedFiles }) => {\n        if (uploadedFiles.length !== 0) {\n          const image = uploadedFiles[0];\n          const imageName = image.originalFile.originalFileName;\n          const imageUrl = UrlBuilder.url({\n            accountId: image.accountId,\n            filePath: image.filePath,\n            options: {\n              transformation: \"preset\",\n              transformationPreset: \"thumbnail\"\n            }\n          });\n          setPhotoName(imageName);\n          setOriginalPhoto(imageUrl);\n          generatePhoto(imageUrl);\n        }\n      }}\n      width=\"670px\"\n      height=\"250px\"\n    />\n  );\n\n  async function generatePhoto(fileUrl: string) {\n    await new Promise((resolve) => setTimeout(resolve, 200));\n    setLoading(true);\n    const res = await fetch(\"/generate\", {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({ imageUrl: fileUrl, theme, room }),\n    });\n\n    let newPhoto = await res.json();\n    if (res.status !== 200) {\n      setError(newPhoto);\n    } else {\n      setRestoredImage(newPhoto[1]);\n    }\n    setTimeout(() => {\n      setLoading(false);\n    }, 1300);\n  }\n\n  return (\n    <div className=\"flex max-w-6xl mx-auto flex-col items-center justify-center py-2 min-h-screen\">\n      <Header />\n      <main className=\"flex flex-1 w-full flex-col items-center justify-center text-center px-4 mt-4 sm:mb-0 mb-8\">\n        <h1 className=\"mx-auto max-w-4xl font-display text-4xl font-bold tracking-normal text-slate-100 sm:text-6xl mb-5\">\n          Generate your <span className=\"text-blue-600\">dream</span> room\n        </h1>\n        <ResizablePanel>\n          <AnimatePresence mode=\"wait\">\n            <motion.div className=\"flex justify-between items-center w-full flex-col mt-4\">\n              {!restoredImage && (\n                <>\n                  <div className=\"space-y-4 w-full max-w-sm\">\n                    <div className=\"flex mt-3 items-center space-x-3\">\n                      <Image\n                        src=\"/number-1-white.svg\"\n                        width={30}\n                        height={30}\n                        alt=\"1 icon\"\n                      />\n                      <p className=\"text-left font-medium\">\n                        Choose your room theme.\n                      </p>\n                    </div>\n                    <DropDown\n                      theme={theme}\n                      setTheme={(newTheme) =>\n                        setTheme(newTheme as typeof theme)\n                      }\n                      themes={themes}\n                    />\n                  </div>\n                  <div className=\"space-y-4 w-full max-w-sm\">\n                    <div className=\"flex mt-10 items-center space-x-3\">\n                      <Image\n                        src=\"/number-2-white.svg\"\n                        width={30}\n                        height={30}\n                        alt=\"1 icon\"\n                      />\n                      <p className=\"text-left font-medium\">\n                        Choose your room type.\n                      </p>\n                    </div>\n                    <DropDown\n                      theme={room}\n                      setTheme={(newRoom) => setRoom(newRoom as typeof room)}\n                      themes={rooms}\n                    />\n                  </div>\n                  <div className=\"mt-4 w-full max-w-sm\">\n                    <div className=\"flex mt-6 w-96 items-center space-x-3\">\n                      <Image\n                        src=\"/number-3-white.svg\"\n                        width={30}\n                        height={30}\n                        alt=\"1 icon\"\n                      />\n                      <p className=\"text-left font-medium\">\n                        Upload a picture of your room.\n                      </p>\n                    </div>\n                  </div>\n                </>\n              )}\n              {restoredImage && (\n                <div>\n                  Here's your remodeled <b>{room.toLowerCase()}</b> in the{\" \"}\n                  <b>{theme.toLowerCase()}</b> theme!{\" \"}\n                </div>\n              )}\n              <div\n                className={`${\n                  restoredLoaded ? \"visible mt-6 -ml-8\" : \"invisible\"\n                }`}\n              >\n                <Toggle\n                  className={`${restoredLoaded ? \"visible mb-6\" : \"invisible\"}`}\n                  sideBySide={sideBySide}\n                  setSideBySide={(newVal) => setSideBySide(newVal)}\n                />\n              </div>\n              {restoredLoaded && sideBySide && (\n                <CompareSlider\n                  original={originalPhoto!}\n                  restored={restoredImage!}\n                />\n              )}\n              {!originalPhoto && <UploadDropZone />}\n              {originalPhoto && !restoredImage && (\n                <Image\n                  alt=\"original photo\"\n                  src={originalPhoto}\n                  className=\"rounded-2xl h-96\"\n                  width={475}\n                  height={475}\n                />\n              )}\n              {restoredImage && originalPhoto && !sideBySide && (\n                <div className=\"flex sm:space-x-4 sm:flex-row flex-col\">\n                  <div>\n                    <h2 className=\"mb-1 font-medium text-lg\">Original Room</h2>\n                    <Image\n                      alt=\"original photo\"\n                      src={originalPhoto}\n                      className=\"rounded-2xl relative w-full h-96\"\n                      width={475}\n                      height={475}\n                    />\n                  </div>\n                  <div className=\"sm:mt-0 mt-8\">\n                    <h2 className=\"mb-1 font-medium text-lg\">Generated Room</h2>\n                    <a href={restoredImage} target=\"_blank\" rel=\"noreferrer\">\n                      <Image\n                        alt=\"restored photo\"\n                        src={restoredImage}\n                        className=\"rounded-2xl relative sm:mt-0 mt-2 cursor-zoom-in w-full h-96\"\n                        width={475}\n                        height={475}\n                        onLoadingComplete={() => setRestoredLoaded(true)}\n                      />\n                    </a>\n                  </div>\n                </div>\n              )}\n              {loading && (\n                <button\n                  disabled\n                  className=\"bg-blue-500 rounded-full text-white font-medium px-4 pt-2 pb-3 mt-8 w-40\"\n                >\n                  <span className=\"pt-4\">\n                    <LoadingDots color=\"white\" style=\"large\" />\n                  </span>\n                </button>\n              )}\n              {error && (\n                <div\n                  className=\"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded-xl mt-8\"\n                  role=\"alert\"\n                >\n                  <span className=\"block sm:inline\">{error}</span>\n                </div>\n              )}\n              <div className=\"flex space-x-2 justify-center\">\n                {originalPhoto && !loading && (\n                  <button\n                    onClick={() => {\n                      setOriginalPhoto(null);\n                      setRestoredImage(null);\n                      setRestoredLoaded(false);\n                      setError(null);\n                    }}\n                    className=\"bg-blue-500 rounded-full text-white font-medium px-4 py-2 mt-8 hover:bg-blue-500/80 transition\"\n                  >\n                    Generate New Room\n                  </button>\n                )}\n                {restoredLoaded && (\n                  <button\n                    onClick={() => {\n                      downloadPhoto(\n                        restoredImage!,\n                        appendNewToName(photoName!)\n                      );\n                    }}\n                    className=\"bg-white rounded-full text-black border font-medium px-4 py-2 mt-8 hover:bg-gray-100 transition\"\n                  >\n                    Download Generated Room\n                  </button>\n                )}\n              </div>\n            </motion.div>\n          </AnimatePresence>\n        </ResizablePanel>\n      </main>\n      <Footer />\n    </div>\n  );\n}\n"
  },
  {
    "path": "app/generate/route.ts",
    "content": "import { Ratelimit } from \"@upstash/ratelimit\";\nimport redis from \"../../utils/redis\";\nimport { NextResponse } from \"next/server\";\nimport { headers } from \"next/headers\";\n\n// Create a new ratelimiter, that allows 5 requests per 24 hours\nconst ratelimit = redis\n  ? new Ratelimit({\n      redis: redis,\n      limiter: Ratelimit.fixedWindow(5, \"1440 m\"),\n      analytics: true,\n    })\n  : undefined;\n\nexport async function POST(request: Request) {\n  // Rate Limiter Code\n  if (ratelimit) {\n    const headersList = headers();\n    const ipIdentifier = headersList.get(\"x-real-ip\");\n\n    const result = await ratelimit.limit(ipIdentifier ?? \"\");\n\n    if (!result.success) {\n      return new Response(\n        \"Too many uploads in 1 day. Please try again in a 24 hours.\",\n        {\n          status: 429,\n          headers: {\n            \"X-RateLimit-Limit\": result.limit,\n            \"X-RateLimit-Remaining\": result.remaining,\n          } as any,\n        }\n      );\n    }\n  }\n\n  const { imageUrl, theme, room } = await request.json();\n\n  // POST request to Replicate to start the image restoration generation process\n  let startResponse = await fetch(\"https://api.replicate.com/v1/predictions\", {\n    method: \"POST\",\n    headers: {\n      \"Content-Type\": \"application/json\",\n      Authorization: \"Token \" + process.env.REPLICATE_API_KEY,\n    },\n    body: JSON.stringify({\n      version:\n        \"854e8727697a057c525cdb45ab037f64ecca770a1769cc52287c2e56472a247b\",\n      input: {\n        image: imageUrl,\n        prompt:\n          room === \"Gaming Room\"\n            ? \"a room for gaming with gaming computers, gaming consoles, and gaming chairs\"\n            : `a ${theme.toLowerCase()} ${room.toLowerCase()}`,\n        a_prompt:\n          \"best quality, extremely detailed, photo from Pinterest, interior, cinematic photo, ultra-detailed, ultra-realistic, award-winning\",\n        n_prompt:\n          \"longbody, lowres, bad anatomy, bad hands, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality\",\n      },\n    }),\n  });\n\n  let jsonStartResponse = await startResponse.json();\n\n  let endpointUrl = jsonStartResponse.urls.get;\n\n  // GET request to get the status of the image restoration process & return the result when it's ready\n  let restoredImage: string | null = null;\n  while (!restoredImage) {\n    // Loop in 1s intervals until the alt text is ready\n    console.log(\"polling for result...\");\n    let finalResponse = await fetch(endpointUrl, {\n      method: \"GET\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Authorization: \"Token \" + process.env.REPLICATE_API_KEY,\n      },\n    });\n    let jsonFinalResponse = await finalResponse.json();\n\n    if (jsonFinalResponse.status === \"succeeded\") {\n      restoredImage = jsonFinalResponse.output;\n    } else if (jsonFinalResponse.status === \"failed\") {\n      break;\n    } else {\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n    }\n  }\n\n  return NextResponse.json(\n    restoredImage ? restoredImage : \"Failed to restore image\"\n  );\n}\n"
  },
  {
    "path": "app/layout.tsx",
    "content": "import { Analytics } from \"@vercel/analytics/react\";\nimport { Metadata } from \"next\";\nimport \"../styles/globals.css\";\n\nlet title = \"Dream Room Generator\";\nlet description = \"Generate your dream room in seconds.\";\nlet ogimage = \"https://roomgpt-demo.vercel.app/og-image.png\";\nlet sitename = \"roomGPT.io\";\n\nexport const metadata: Metadata = {\n  title,\n  description,\n  icons: {\n    icon: \"/favicon.ico\",\n  },\n  openGraph: {\n    images: [ogimage],\n    title,\n    description,\n    url: \"https://roomgpt-demo.vercel.app\",\n    siteName: sitename,\n    locale: \"en_US\",\n    type: \"website\",\n  },\n  twitter: {\n    card: \"summary_large_image\",\n    images: [ogimage],\n    title,\n    description,\n  },\n};\n\nexport default function RootLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <html lang=\"en\">\n      <body className=\"bg-[#17181C] text-white\">\n        {children}\n        <Analytics />\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "app/page.tsx",
    "content": "import Image from \"next/image\";\nimport Link from \"next/link\";\nimport Footer from \"../components/Footer\";\nimport Header from \"../components/Header\";\nimport SquigglyLines from \"../components/SquigglyLines\";\n\nexport default function HomePage() {\n  return (\n    <div className=\"flex max-w-6xl mx-auto flex-col items-center justify-center py-2 min-h-screen\">\n      <Header />\n      <main className=\"flex flex-1 w-full flex-col items-center justify-center text-center px-4 sm:mt-20 mt-20 background-gradient\">\n        <a\n          href=\"https://vercel.fyi/roomGPT\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"border border-gray-700 rounded-lg py-2 px-4 text-gray-400 text-sm mb-5 transition duration-300 ease-in-out\"\n        >\n          Clone and deploy your own with{\" \"}\n          <span className=\"text-blue-600\">Vercel</span>\n        </a>\n        <h1 className=\"mx-auto max-w-4xl font-display text-5xl font-bold tracking-normal text-gray-300 sm:text-7xl\">\n          Generating dream rooms{\" \"}\n          <span className=\"relative whitespace-nowrap text-blue-600\">\n            <SquigglyLines />\n            <span className=\"relative\">using AI</span>\n          </span>{\" \"}\n          for everyone.\n        </h1>\n        <h2 className=\"mx-auto mt-12 max-w-xl text-lg sm:text-gray-400  text-gray-500 leading-7\">\n          Take a picture of your room and see how your room looks in different\n          themes. 100% free – remodel your room today.\n        </h2>\n        <Link\n          className=\"bg-blue-600 rounded-xl text-white font-medium px-4 py-3 sm:mt-10 mt-8 hover:bg-blue-500 transition\"\n          href=\"/dream\"\n        >\n          Generate your dream room\n        </Link>\n        <div className=\"flex justify-between items-center w-full flex-col sm:mt-10 mt-6\">\n          <div className=\"flex flex-col space-y-10 mt-4 mb-16\">\n            <div className=\"flex sm:space-x-8 sm:flex-row flex-col\">\n              <div>\n                <h3 className=\"mb-1 font-medium text-lg\">Original Room</h3>\n                <Image\n                  alt=\"Original photo of a room with roomGPT.io\"\n                  src=\"/original-pic.jpg\"\n                  className=\"w-full object-cover h-96 rounded-2xl\"\n                  width={400}\n                  height={400}\n                />\n              </div>\n              <div className=\"sm:mt-0 mt-8\">\n                <h3 className=\"mb-1 font-medium text-lg\">Generated Room</h3>\n                <Image\n                  alt=\"Generated photo of a room with roomGPT.io\"\n                  width={400}\n                  height={400}\n                  src=\"/generated-pic-2.jpg\"\n                  className=\"w-full object-cover h-96 rounded-2xl sm:mt-0 mt-2\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n      </main>\n      <Footer />\n    </div>\n  );\n}\n"
  },
  {
    "path": "components/CompareSlider.tsx",
    "content": "import {\n  ReactCompareSlider,\n  ReactCompareSliderImage,\n} from \"react-compare-slider\";\n\nexport const CompareSlider = ({\n  original,\n  restored,\n}: {\n  original: string;\n  restored: string;\n}) => {\n  return (\n    <ReactCompareSlider\n      itemOne={<ReactCompareSliderImage src={original} alt=\"original photo\" />}\n      itemTwo={<ReactCompareSliderImage src={restored} alt=\"generated photo\" />}\n      portrait\n      className=\"flex w-[600px] mt-5 h-96\"\n    />\n  );\n};\n"
  },
  {
    "path": "components/DropDown.tsx",
    "content": "import { Menu, Transition } from \"@headlessui/react\";\nimport {\n  CheckIcon,\n  ChevronDownIcon,\n  ChevronUpIcon,\n} from \"@heroicons/react/20/solid\";\nimport { Fragment } from \"react\";\nimport { roomType, themeType } from \"../utils/dropdownTypes\";\n\nfunction classNames(...classes: string[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n\ninterface DropDownProps {\n  theme: themeType | roomType;\n  setTheme: (theme: themeType | roomType) => void;\n  themes: themeType[] | roomType[];\n}\n\n// TODO: Change names since this is a generic dropdown now\nexport default function DropDown({ theme, setTheme, themes }: DropDownProps) {\n  return (\n    <Menu as=\"div\" className=\"relative block text-left\">\n      <div>\n        <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\">\n          {theme}\n          <ChevronUpIcon\n            className=\"-mr-1 ml-2 h-5 w-5 ui-open:hidden\"\n            aria-hidden=\"true\"\n          />\n          <ChevronDownIcon\n            className=\"-mr-1 ml-2 h-5 w-5 hidden ui-open:block\"\n            aria-hidden=\"true\"\n          />\n        </Menu.Button>\n      </div>\n\n      <Transition\n        as={Fragment}\n        enter=\"transition ease-out duration-100\"\n        enterFrom=\"transform opacity-0 scale-95\"\n        enterTo=\"transform opacity-100 scale-100\"\n        leave=\"transition ease-in duration-75\"\n        leaveFrom=\"transform opacity-100 scale-100\"\n        leaveTo=\"transform opacity-0 scale-95\"\n      >\n        <Menu.Items\n          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\"\n          key={theme}\n        >\n          <div className=\"\">\n            {themes.map((themeItem) => (\n              <Menu.Item key={themeItem}>\n                {({ active }) => (\n                  <button\n                    onClick={() => setTheme(themeItem)}\n                    className={classNames(\n                      active ? \"bg-gray-100 text-gray-900\" : \"text-gray-700\",\n                      themeItem === theme ? \"bg-gray-200\" : \"\",\n                      \"px-4 py-2 text-sm w-full text-left flex items-center space-x-2 justify-between\"\n                    )}\n                  >\n                    <span>{themeItem}</span>\n                    {themeItem === theme ? (\n                      <CheckIcon className=\"w-4 h-4 text-bold\" />\n                    ) : null}\n                  </button>\n                )}\n              </Menu.Item>\n            ))}\n          </div>\n        </Menu.Items>\n      </Transition>\n    </Menu>\n  );\n}\n"
  },
  {
    "path": "components/Footer.tsx",
    "content": "import Link from \"next/link\";\n\nexport default function Footer() {\n  return (\n    <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\">\n      <div className=\"text-gray-500\">\n        Powered by{\" \"}\n        <a\n          href=\"https://replicate.com/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"font-bold hover:underline transition hover:text-gray-300 underline-offset-2\"\n        >\n          Replicate,{\" \"}\n        </a>\n        <a\n          href=\"https://www.bytescale.com/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"font-bold hover:underline hover:text-gray-300 transition underline-offset-2\"\n        >\n          Bytescale,{\" \"}\n        </a>\n        and{\" \"}\n        <a\n          href=\"https://vercel.com/\"\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"font-bold hover:underline transition hover:text-gray-300 underline-offset-2\"\n        >\n          Vercel.\n        </a>\n      </div>\n      <div className=\"flex space-x-4 pb-4 sm:pb-0\">\n        <Link\n          href=\"https://twitter.com/nutlope\"\n          className=\"group\"\n          aria-label=\"TaxPal on Twitter\"\n        >\n          <svg\n            aria-hidden=\"true\"\n            className=\"h-6 w-6 fill-gray-500 group-hover:fill-gray-300\"\n          >\n            <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\" />\n          </svg>\n        </Link>\n        <Link\n          href=\"https://github.com/Nutlope/roomGPT\"\n          className=\"group\"\n          aria-label=\"TaxPal on GitHub\"\n        >\n          <svg\n            aria-hidden=\"true\"\n            className=\"h-6 w-6 fill-gray-500 group-hover:fill-gray-300\"\n          >\n            <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\" />\n          </svg>\n        </Link>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "components/Header.tsx",
    "content": "import Image from \"next/image\";\nimport Link from \"next/link\";\n\nexport default function Header() {\n  return (\n    <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\">\n      <Link href=\"/\" className=\"flex space-x-2\">\n        <Image\n          alt=\"header text\"\n          src=\"/bed.svg\"\n          className=\"sm:w-10 sm:h-10 w-9 h-9\"\n          width={24}\n          height={24}\n        />\n        <h1 className=\"sm:text-3xl text-xl font-bold ml-2 tracking-tight\">\n          roomGPT.io\n        </h1>\n      </Link>\n      <a\n        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\"\n        href=\"https://github.com/Nutlope/roomGPT\"\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n      >\n        <Github />\n        <p>Star on GitHub</p>\n      </a>\n    </header>\n  );\n}\n\nfunction Github({ className }: { className?: string }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"20\"\n      height=\"20\"\n      fill=\"currentColor\"\n      viewBox=\"0 0 24 24\"\n      className={className}\n    >\n      <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\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "components/LoadingDots.tsx",
    "content": "import styles from \"../styles/loading-dots.module.css\";\n\nconst LoadingDots = ({\n  color = \"#000\",\n  style = \"small\",\n}: {\n  color: string;\n  style: string;\n}) => {\n  return (\n    <span className={style == \"small\" ? styles.loading2 : styles.loading}>\n      <span style={{ backgroundColor: color }} />\n      <span style={{ backgroundColor: color }} />\n      <span style={{ backgroundColor: color }} />\n    </span>\n  );\n};\n\nexport default LoadingDots;\n\nLoadingDots.defaultProps = {\n  style: \"small\",\n};\n"
  },
  {
    "path": "components/ResizablePanel.tsx",
    "content": "import { motion } from \"framer-motion\";\nimport useMeasure from \"react-use-measure\";\n\nexport default function ResizablePanel({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  let [ref, { height }] = useMeasure();\n\n  return (\n    <motion.div\n      animate={height ? { height } : {}}\n      style={height ? { height } : {}}\n      className=\"relative w-full overflow-hidden\"\n      transition={{ type: \"tween\", duration: 0.5 }}\n    >\n      <div ref={ref} className={height ? \"absolute inset-x-0\" : \"relative\"}>\n        {children}\n      </div>\n    </motion.div>\n  );\n}\n"
  },
  {
    "path": "components/SquigglyLines.tsx",
    "content": "export default function SquigglyLines() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 418 42\"\n      className=\"absolute top-2/3 left-0 h-[0.58em] w-full fill-blue-500/60\"\n      preserveAspectRatio=\"none\"\n    >\n      <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\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "components/Toggle.tsx",
    "content": "import { Switch } from \"@headlessui/react\";\n\nfunction classNames(...classes: string[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n\nexport interface ToggleProps extends React.HTMLAttributes<HTMLDivElement> {\n  sideBySide: boolean;\n  setSideBySide: (sideBySide: boolean) => void;\n}\n\nexport default function Toggle({\n  sideBySide,\n  setSideBySide,\n  ...props\n}: ToggleProps) {\n  return (\n    <Switch.Group as=\"div\" {...props}>\n      <div className=\"flex items-center\">\n        <span\n          className={`text-sm mr-3 font-medium ${\n            !sideBySide ? \"text-white\" : \"text-gray-500\"\n          }`}\n        >\n          Side by Side\n        </span>\n        <Switch\n          checked={sideBySide}\n          onChange={setSideBySide}\n          className={classNames(\n            sideBySide ? \"bg-blue-600\" : \"bg-gray-200\",\n            \"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 \"\n          )}\n        >\n          <span\n            aria-hidden=\"true\"\n            className={classNames(\n              sideBySide ? \"translate-x-5\" : \"translate-x-0\",\n              \"pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out\"\n            )}\n          />\n        </Switch>\n        <Switch.Label as=\"span\" className=\"ml-3\">\n          <span\n            className={`text-sm font-medium ${\n              sideBySide ? \"text-white\" : \"text-gray-500\"\n            } `}\n          >\n            Compare\n          </span>\n        </Switch.Label>\n      </div>\n    </Switch.Group>\n  );\n}\n"
  },
  {
    "path": "next.config.js",
    "content": "/** @type {import('next').NextConfig} */\nmodule.exports = {\n  reactStrictMode: true,\n  images: {\n    domains: [\"upcdn.io\", \"replicate.delivery\"],\n  },\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\"\n  },\n  \"dependencies\": {\n    \"@bytescale/upload-widget-react\": \"^4.9.0\",\n    \"@headlessui/react\": \"^1.7.7\",\n    \"@headlessui/tailwindcss\": \"^0.1.2\",\n    \"@heroicons/react\": \"^2.0.16\",\n    \"@tailwindcss/forms\": \"^0.5.3\",\n    \"@upstash/ratelimit\": \"^0.3.8\",\n    \"@upstash/redis\": \"^1.19.1\",\n    \"@vercel/analytics\": \"^0.1.11\",\n    \"framer-motion\": \"^8.2.4\",\n    \"next\": \"^13.4.4\",\n    \"react\": \"^18.2.0\",\n    \"react-compare-slider\": \"^2.2.0\",\n    \"react-countup\": \"^6.4.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-use-measure\": \"^2.1.1\",\n    \"request-ip\": \"^3.3.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"18.11.3\",\n    \"@types/react\": \"18.0.21\",\n    \"@types/react-dom\": \"18.0.6\",\n    \"@types/request-ip\": \"^0.0.37\",\n    \"autoprefixer\": \"^10.4.12\",\n    \"postcss\": \"^8.4.18\",\n    \"tailwindcss\": \"^3.2.4\",\n    \"typescript\": \"4.9.4\"\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "styles/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@media (min-width: 400px) {\n  .background-gradient::before {\n    background: radial-gradient(\n      20% 50% at 50% 50%,\n      rgba(71, 127, 247, 0.376) 0%,\n      rgba(37, 38, 44, 0) 100%\n    );\n    z-index: -10;\n    content: \"\";\n    position: absolute;\n    inset: 0px;\n    transform: scale(1);\n    pointer-events: none;\n  }\n}\n"
  },
  {
    "path": "styles/loading-dots.module.css",
    "content": ".loading {\n  display: inline-flex;\n  align-items: center;\n}\n\n.loading .spacer {\n  margin-right: 2px;\n}\n\n.loading span {\n  animation-name: blink;\n  animation-duration: 1.4s;\n  animation-iteration-count: infinite;\n  animation-fill-mode: both;\n  width: 5px;\n  height: 5px;\n  border-radius: 50%;\n  display: inline-block;\n  margin: 0 1px;\n}\n\n.loading span:nth-of-type(2) {\n  animation-delay: 0.2s;\n}\n\n.loading span:nth-of-type(3) {\n  animation-delay: 0.4s;\n}\n\n.loading2 {\n  display: inline-flex;\n  align-items: center;\n}\n\n.loading2 .spacer {\n  margin-right: 2px;\n}\n\n.loading2 span {\n  animation-name: blink;\n  animation-duration: 1.4s;\n  animation-iteration-count: infinite;\n  animation-fill-mode: both;\n  width: 4px;\n  height: 4px;\n  border-radius: 50%;\n  display: inline-block;\n  margin: 0 1px;\n}\n\n.loading2 span:nth-of-type(2) {\n  animation-delay: 0.2s;\n}\n\n.loading2 span:nth-of-type(3) {\n  animation-delay: 0.4s;\n}\n\n@keyframes blink {\n  0% {\n    opacity: 0.2;\n  }\n  20% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0.2;\n  }\n}\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\n    \"./pages/**/*.{js,ts,jsx,tsx}\",\n    \"./components/**/*.{js,ts,jsx,tsx}\",\n    \"./app/**/*.{js,ts,jsx,tsx}\",\n  ],\n  theme: {\n    extend: {\n      screens: {\n        xs: \"330px\",\n      },\n    },\n  },\n  plugins: [require(\"@tailwindcss/forms\"), require(\"@headlessui/tailwindcss\")],\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ]\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\",\n    \".next/types/**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  },
  {
    "path": "utils/appendNewToName.ts",
    "content": "export default function appendNewToName(name: string) {\n  let insertPos = name.indexOf(\".\");\n  let newName = name\n    .substring(0, insertPos)\n    .concat(\"-new\", name.substring(insertPos));\n  return newName;\n}\n"
  },
  {
    "path": "utils/downloadPhoto.ts",
    "content": "function forceDownload(blobUrl: string, filename: string) {\n  let a: any = document.createElement(\"a\");\n  a.download = filename;\n  a.href = blobUrl;\n  document.body.appendChild(a);\n  a.click();\n  a.remove();\n}\n\nexport default function downloadPhoto(url: string, filename: string) {\n  fetch(url, {\n    headers: new Headers({\n      Origin: location.origin,\n    }),\n    mode: \"cors\",\n  })\n    .then((response) => response.blob())\n    .then((blob) => {\n      let blobUrl = window.URL.createObjectURL(blob);\n      forceDownload(blobUrl, filename);\n    })\n    .catch((e) => console.error(e));\n}\n"
  },
  {
    "path": "utils/dropdownTypes.ts",
    "content": "export type themeType =\n  | \"Modern\"\n  | \"Vintage\"\n  | \"Minimalist\"\n  | \"Professional\"\n  | \"Tropical\";\n\nexport type roomType =\n  | \"Living Room\"\n  | \"Dining Room\"\n  | \"Bedroom\"\n  | \"Bathroom\"\n  | \"Office\"\n  | \"Gaming Room\";\n\nexport const themes: themeType[] = [\n  \"Modern\",\n  \"Minimalist\",\n  \"Professional\",\n  \"Tropical\",\n  \"Vintage\",\n];\nexport const rooms: roomType[] = [\n  \"Living Room\",\n  \"Dining Room\",\n  \"Office\",\n  \"Bedroom\",\n  \"Bathroom\",\n  \"Gaming Room\",\n];\n"
  },
  {
    "path": "utils/redis.ts",
    "content": "import { Redis } from \"@upstash/redis\";\n\nconst redis =\n  !!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_REDIS_REST_TOKEN\n    ? new Redis({\n        url: process.env.UPSTASH_REDIS_REST_URL,\n        token: process.env.UPSTASH_REDIS_REST_TOKEN,\n      })\n    : undefined;\n\nexport default redis;\n"
  }
]