diff --git a/package.json b/package.json index 9d4eb45..07c21d0 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "openai": "^3.1.0", "plaiceholder": "^2.5.0", "react": "18.2.0", + "react-advanced-cropper": "^0.17.0", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-icons": "^4.7.1", diff --git a/src/components/dashboard/Uploader.tsx b/src/components/dashboard/Uploader.tsx index 8f9219f..7b9230e 100644 --- a/src/components/dashboard/Uploader.tsx +++ b/src/components/dashboard/Uploader.tsx @@ -1,4 +1,4 @@ -import { resizeImage } from "@/core/utils/upload"; +import { createPreviewMedia, resizeImage } from "@/core/utils/upload"; import { Box, Button, @@ -30,11 +30,12 @@ import { CheckedListItem } from "../home/Pricing"; import UploadErrorMessages from "./UploadErrorMessages"; type TUploadState = "not_uploaded" | "uploading" | "uploaded"; +export type FilePreview = (File | Blob) & { preview: string }; const MAX_FILES = 25; const Uploader = ({ handleOnAdd }: { handleOnAdd: () => void }) => { - const [files, setFiles] = useState<(File & { preview: string })[]>([]); + const [files, setFiles] = useState([]); const [uploadState, setUploadState] = useState("not_uploaded"); const [errorMessages, setErrorMessages] = useState([]); const [urls, setUrls] = useState([]); @@ -74,11 +75,7 @@ const Uploader = ({ handleOnAdd }: { handleOnAdd: () => void }) => { setErrorMessages([]); setFiles([ ...files, - ...acceptedFiles.map((file) => - Object.assign(file, { - preview: URL.createObjectURL(file), - }) - ), + ...acceptedFiles.map((file) => createPreviewMedia(file)), ]); } }, diff --git a/src/components/layout/AuthForm.tsx b/src/components/layout/AuthForm.tsx index 8b01a45..7a2f05f 100644 --- a/src/components/layout/AuthForm.tsx +++ b/src/components/layout/AuthForm.tsx @@ -1,33 +1,56 @@ +import { getEmailProvider } from "@/core/utils/mail"; import { Box, Button, FormControl, FormLabel, + Heading, + Icon, Input, + Link, Stack, Text, } from "@chakra-ui/react"; import { signIn } from "next-auth/react"; -import { useRouter } from "next/router"; import { useState } from "react"; import { FaPaperPlane } from "react-icons/fa"; +import { MdCheckCircleOutline } from "react-icons/md"; import { useMutation } from "react-query"; export default function AuthForm() { const [email, setEmail] = useState(""); - const router = useRouter(); - - const { mutate: login, isLoading } = useMutation( - "login", - () => - signIn("email", { email, redirect: false, callbackUrl: "/dashboard" }), - { - onSuccess: () => { - router.push("/login?verifyRequest=1"); - }, - } + const { + mutate: login, + isLoading, + isSuccess, + } = useMutation("login", () => + signIn("email", { email, redirect: false, callbackUrl: "/dashboard" }) ); + if (isSuccess) { + const { name, url } = getEmailProvider(email); + + return ( + + + Check your email + + + A sign in link has been sent to your email address.{" "} + {name && url && ( + <> + Check{" "} + + your {name} inbox + + . + + )} + + + ); + } + return ( diff --git a/src/components/projects/PomptWizardPopover.tsx b/src/components/projects/PomptWizardPopover.tsx deleted file mode 100644 index e692644..0000000 --- a/src/components/projects/PomptWizardPopover.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import useProjectContext from "@/hooks/use-project-context"; -import { - Badge, - Button, - ButtonGroup, - Input, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Text, - useDisclosure, -} from "@chakra-ui/react"; -import axios from "axios"; -import { useRouter } from "next/router"; -import { useState } from "react"; -import { FaMagic } from "react-icons/fa"; -import { useMutation } from "react-query"; - -const PomptWizardPopover = () => { - const { query } = useRouter(); - const { promptInputRef, updatePromptWizardCredits, promptWizardCredits } = - useProjectContext(); - const { isOpen, onOpen, onClose } = useDisclosure(); - const [keyword, setKeyword] = useState(""); - - const { mutate: createPrompt, isLoading: isLoadingPrompt } = useMutation( - "create-prompt", - (keyword: string) => - axios.post(`/api/projects/${query.id}/prompter`, { - keyword, - }), - { - onSuccess: (response) => { - const { prompt } = response.data; - promptInputRef.current!.value = prompt; - updatePromptWizardCredits(response.data.promptWizardCredits); - setKeyword(""); - onClose(); - }, - } - ); - - return ( - <> - - - - - { - e.preventDefault(); - e.stopPropagation(); - - if (keyword) { - createPrompt(keyword); - } - }} - as="form" - > - Prompt Wizard - - - - Enter a topic or concept and our AI will generate a good - prompt example based on it: - - setKeyword(e.currentTarget.value)} - /> - - {promptWizardCredits} prompt assist - {promptWizardCredits && "s"} left - - - - - - - - - - - - - ); -}; - -export default PomptWizardPopover; diff --git a/src/components/projects/ProjectCard.tsx b/src/components/projects/ProjectCard.tsx index 9022161..e995321 100644 --- a/src/components/projects/ProjectCard.tsx +++ b/src/components/projects/ProjectCard.tsx @@ -74,7 +74,7 @@ const ProjectCard = ({ {project.credits} shots left )} - + {formatRelative(new Date(project.createdAt), new Date())} diff --git a/src/components/projects/PromptImage.tsx b/src/components/projects/PromptImage.tsx new file mode 100644 index 0000000..3a11fde --- /dev/null +++ b/src/components/projects/PromptImage.tsx @@ -0,0 +1,164 @@ +import { createPreviewMedia, resizeImage } from "@/core/utils/upload"; +import useProjectContext from "@/hooks/use-project-context"; +import { + Button, + ButtonGroup, + Center, + Icon, + Modal, + ModalBody, + ModalCloseButton, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, + useDisclosure, +} from "@chakra-ui/react"; +import { useS3Upload } from "next-s3-upload"; +import { useRef, useState } from "react"; +import { + FixedCropper, + FixedCropperRef, + ImageRestriction, +} from "react-advanced-cropper"; +import { useDropzone } from "react-dropzone"; +import { + BsCloud, + BsCloudArrowDown, + BsCloudArrowUp, + BsImage, +} from "react-icons/bs"; +import { FilePreview } from "../dashboard/Uploader"; + +import "react-advanced-cropper/dist/style.css"; +import "react-advanced-cropper/dist/themes/compact.css"; + +const PromptImage = () => { + const [isLoading, setLoading] = useState(false); + const { isOpen, onOpen, onClose } = useDisclosure({ + onClose: () => { + setImagePreview(undefined); + }, + }); + + const { setPromptImageUrl } = useProjectContext(); + const cropperRef = useRef(null); + + const [imagePreview, setImagePreview] = useState(); + + const { uploadToS3 } = useS3Upload(); + + const { getRootProps, getInputProps } = useDropzone({ + accept: { + "image/png": [".png"], + "image/jpeg": [".jpeg", ".jpg"], + }, + maxSize: 10000000, + multiple: false, + onDrop: (acceptedFiles) => { + if (acceptedFiles?.[0]) { + setImagePreview(createPreviewMedia(acceptedFiles[0])); + } + }, + }); + + const handleSubmit = async () => { + if (!cropperRef.current) { + return; + } + + setLoading(true); + const canvas = cropperRef.current.getCanvas({ + height: 512, + width: 512, + })!; + + canvas.toBlob(async (blob) => { + const croppedImage = createPreviewMedia(blob!); + const file = await resizeImage(croppedImage as File); + + const { url } = await uploadToS3(file); + setLoading(false); + + setPromptImageUrl(url); + onClose(); + }, "image/jpeg"); + }; + + return ( + <> + + + + + { + e.preventDefault(); + e.stopPropagation(); + + handleSubmit(); + }} + > + Use image as model + + + {imagePreview ? ( + + ) : ( +
+ + + Upload reference image +
+ )} +
+ + + + + + + +
+
+ + ); +}; + +export default PromptImage; diff --git a/src/components/projects/PromptPanel.tsx b/src/components/projects/PromptPanel.tsx index 23938d4..ace7f3c 100644 --- a/src/components/projects/PromptPanel.tsx +++ b/src/components/projects/PromptPanel.tsx @@ -17,10 +17,14 @@ import Image from "next/image"; import { BsLightbulb } from "react-icons/bs"; import { FaCameraRetro } from "react-icons/fa"; import { useMutation } from "react-query"; -import PomptWizardPopover from "./PomptWizardPopover"; import PromptsDrawer from "./PromptsDrawer"; +import PromptImage from "./PromptImage"; -const PromptPanel = () => { +const PromptPanel = ({ + hasImageInputAvailable, +}: { + hasImageInputAvailable: Boolean; +}) => { const { project, shotCredits, @@ -30,6 +34,8 @@ const PromptPanel = () => { updateShotTemplate, promptInputRef, updatePromptWizardCredits, + promptImageUrl, + setPromptImageUrl, } = useProjectContext(); const { mutate: createPrediction, isLoading: isCreatingPrediction } = @@ -39,11 +45,13 @@ const PromptPanel = () => { axios.post<{ shot: Shot }>(`/api/projects/${project.id}/predictions`, { prompt: promptInputRef.current!.value, seed: shotTemplate?.seed, + ...(promptImageUrl && { image: promptImageUrl }), }), { onSuccess: (response) => { addShot(response.data.shot); promptInputRef.current!.value = ""; + setPromptImageUrl(undefined); }, } ); @@ -78,7 +86,7 @@ const PromptPanel = () => { - + {hasImageInputAvailable && } { - {shotTemplate ? ( + {promptImageUrl && ( + + prompt + + The new shot will use this image as a guide (image to image + mode) +
+ +
+
+ )} + {shotTemplate && ( { height={48} /> - The new shot will use the same style as this image (using - the same seed) + The new shot will use the same style as this image (same + seed)
- ) : ( + )} + + {!shotTemplate && !promptImageUrl && ( diff --git a/src/components/projects/PromptWizardPanel.tsx b/src/components/projects/PromptWizardPanel.tsx new file mode 100644 index 0000000..ba00b3c --- /dev/null +++ b/src/components/projects/PromptWizardPanel.tsx @@ -0,0 +1,75 @@ +import useProjectContext from "@/hooks/use-project-context"; +import { Button, Input, Text, VStack } from "@chakra-ui/react"; +import axios from "axios"; +import { useRouter } from "next/router"; +import { useState } from "react"; +import { FaMagic } from "react-icons/fa"; +import { useMutation } from "react-query"; + +const PromptWizardPanel = ({ onClose }: { onClose: () => void }) => { + const { query } = useRouter(); + const { promptInputRef, updatePromptWizardCredits, promptWizardCredits } = + useProjectContext(); + const [keyword, setKeyword] = useState(""); + + const { mutate: createPrompt, isLoading: isLoadingPrompt } = useMutation( + "create-prompt", + (keyword: string) => + axios.post(`/api/projects/${query.id}/prompter`, { + keyword, + }), + { + onSuccess: (response) => { + const { prompt } = response.data; + promptInputRef.current!.value = prompt; + updatePromptWizardCredits(response.data.promptWizardCredits); + setKeyword(""); + onClose(); + }, + } + ); + + return ( + { + e.preventDefault(); + e.stopPropagation(); + + if (keyword) { + createPrompt(keyword); + } + }} + > + + Enter a topic or concept and our AI will generate a good prompt + example based on it: + + setKeyword(e.currentTarget.value)} + /> + + {promptWizardCredits} prompt assist + {promptWizardCredits && "s"} left + + + + + ); +}; + +export default PromptWizardPanel; diff --git a/src/components/projects/PromptsDrawer.tsx b/src/components/projects/PromptsDrawer.tsx index 82b5308..210a677 100644 --- a/src/components/projects/PromptsDrawer.tsx +++ b/src/components/projects/PromptsDrawer.tsx @@ -3,6 +3,7 @@ import useProjectContext from "@/hooks/use-project-context"; import { Box, Button, + Divider, Drawer, DrawerBody, DrawerCloseButton, @@ -12,8 +13,11 @@ import { SimpleGrid, Text, useDisclosure, + VStack, } from "@chakra-ui/react"; import Image from "next/image"; +import { FaMagic } from "react-icons/fa"; +import PromptWizardPanel from "./PromptWizardPanel"; const PromptsDrawer = () => { const { isOpen, onOpen, onClose } = useDisclosure(); @@ -21,8 +25,13 @@ const PromptsDrawer = () => { return ( <> - { - Select style + Prompt Assistant - - {prompts.map((prompt) => ( - - { - promptInputRef.current!.value = prompt.prompt; - onClose(); - }} - style={{ borderRadius: 10 }} - src={`/prompts/sacha/${prompt.slug}.png`} - alt={prompt.label} - width="400" - height="400" - /> - - {prompt.label} - - - ))} - + } + spacing={6} + > + + + Or select a preset: + + {prompts.map((prompt) => ( + + { + promptInputRef.current!.value = prompt.prompt; + onClose(); + }} + style={{ borderRadius: 10 }} + src={`/prompts/sacha/${prompt.slug}.png`} + alt={prompt.label} + width="400" + height="400" + /> + + {prompt.label} + + + ))} + + + diff --git a/src/components/projects/shot/ShotCard.tsx b/src/components/projects/shot/ShotCard.tsx index d160687..b9eada3 100644 --- a/src/components/projects/shot/ShotCard.tsx +++ b/src/components/projects/shot/ShotCard.tsx @@ -5,12 +5,14 @@ import { Center, Flex, HStack, + Icon, IconButton, Link, Spinner, Text, Tooltip, useClipboard, + VStack, } from "@chakra-ui/react"; import { Shot } from "@prisma/client"; import axios from "axios"; @@ -23,6 +25,7 @@ import { IoMdCheckmarkCircleOutline } from "react-icons/io"; import { MdOutlineModelTraining } from "react-icons/md"; import { useMutation, useQuery } from "react-query"; import ShotImage from "./ShotImage"; +import { TbFaceIdError } from "react-icons/tb"; const ShotCard = ({ shot: initialShot, @@ -83,9 +86,20 @@ const ShotCard = ({ ) : ( -
- -
+ {shot.status === "failed" ? ( +
+ + + + Shot generation failed + + +
+ ) : ( +
+ +
+ )}
)} diff --git a/src/contexts/project-context.tsx b/src/contexts/project-context.tsx index fc88c1a..c35fe79 100644 --- a/src/contexts/project-context.tsx +++ b/src/contexts/project-context.tsx @@ -1,5 +1,5 @@ import { IStudioPageProps } from "@/pages/studio/[id]"; -import { Project, Shot } from "@prisma/client"; +import { Shot } from "@prisma/client"; import axios from "axios"; import { createContext, ReactNode, RefObject, useRef, useState } from "react"; import { useQuery } from "react-query"; @@ -20,6 +20,8 @@ export const ProjectContext = createContext<{ updateShotTemplate: (shot: Shot | undefined) => void; fetchShots: () => void; promptInputRef: RefObject; + promptImageUrl: string | undefined; + setPromptImageUrl: (promptImage: string | undefined) => void; }>(null!); export const ProjectProvider = ({ @@ -32,6 +34,7 @@ export const ProjectProvider = ({ const promptInputRef = useRef(null); const [shots, setShots] = useState(project.shots); const [shotTemplate, setShotTemplate] = useState(); + const [promptImageUrl, setPromptImageUrl] = useState(); const [skip, setSkip] = useState(PROJECTS_PER_PAGE); const [hasMoreResult, setHasMoreResult] = useState( project.shots.length < project._count.shots @@ -104,6 +107,8 @@ export const ProjectProvider = ({ promptInputRef, promptWizardCredits, updatePromptWizardCredits, + promptImageUrl, + setPromptImageUrl, }} > {children} diff --git a/src/core/utils/mail.ts b/src/core/utils/mail.ts new file mode 100644 index 0000000..e734c64 --- /dev/null +++ b/src/core/utils/mail.ts @@ -0,0 +1,214 @@ +// from https://github.com/fnando/email-provider-info + +export type EmailProvider = { + name: string; + url: string; + hosts: string[]; +}; + +export const providers: EmailProvider[] = [ + { + name: "Gmail", + url: "https://mail.google.com/", + hosts: ["gmail.com", "googlemail.com"], + }, + + { + name: "Yahoo!", + url: "https://mail.yahoo.com/", + hosts: [ + "yahoo.com", + "yahoo.es", + "yahoo.it", + "yahoo.de", + "yahoo.fr", + "yahoo.in", + "yahoo.ca", + "yahoo.com.br", + "yahoo.com.au", + "yahoo.com.ar", + "yahoo.com.mx", + "yahoo.com.sg", + "yahoo.co.id", + "yahoo.co.in", + "yahoo.co.jp", + "yahoo.co.uk", + ], + }, + + { + name: "Fastmail", + url: "https://www.fastmail.com/mail/", + hosts: [ + "123mail.org", + "150mail.com", + "150ml.com", + "16mail.com", + "2-mail.com", + "4email.net", + "50mail.com", + "airpost.net", + "allmail.net", + "bestmail.us", + "cluemail.com", + "elitemail.org", + "emailcorner.net", + "emailengine.net", + "emailengine.org", + "emailgroups.net", + "emailplus.org", + "emailuser.net", + "eml.cc", + "f-m.fm", + "fast-email.com", + "fast-mail.org", + "fastem.com", + "fastemail.us", + "fastemailer.com", + "fastest.cc", + "fastimap.com", + "fastmail.cn", + "fastmail.co.uk", + "fastmail.com", + "fastmail.com.au", + "fastmail.de", + "fastmail.es", + "fastmail.fm", + "fastmail.fr", + "fastmail.im", + "fastmail.in", + "fastmail.jp", + "fastmail.mx", + "fastmail.net", + "fastmail.nl", + "fastmail.org", + "fastmail.se", + "fastmail.to", + "fastmail.tw", + "fastmail.uk", + "fastmail.us", + "fastmailbox.net", + "fastmessaging.com", + "fea.st", + "fmail.co.uk", + "fmailbox.com", + "fmgirl.com", + "fmguy.com", + "ftml.net", + "h-mail.us", + "hailmail.net", + "imap-mail.com", + "imap.cc", + "imapmail.org", + "inoutbox.com", + "internet-e-mail.com", + "internet-mail.org", + "internetemails.net", + "internetmailing.net", + "jetemail.net", + "justemail.net", + "letterboxes.org", + "mail-central.com", + "mail-page.com", + "mailandftp.com", + "mailas.com", + "mailbolt.com", + "mailc.net", + "mailcan.com", + "mailforce.net", + "mailftp.com", + "mailhaven.com", + "mailingaddress.org", + "mailite.com", + "mailmight.com", + "mailnew.com", + "mailsent.net", + "mailservice.ms", + "mailup.net", + "mailworks.org", + "ml1.net", + "mm.st", + "myfastmail.com", + "mymacmail.com", + "nospammail.net", + "ownmail.net", + "petml.com", + "postinbox.com", + "postpro.net", + "proinbox.com", + "promessage.com", + "realemail.net", + "reallyfast.biz", + "reallyfast.info", + "rushpost.com", + "sent.as", + "sent.at", + "sent.com", + "speedpost.net", + "speedymail.org", + "ssl-mail.com", + "swift-mail.com", + "the-fastest.net", + "the-quickest.com", + "theinternetemail.com", + "veryfast.biz", + "veryspeedy.net", + "warpmail.net", + "xsmail.com", + "yepmail.net", + "your-mail.com", + ], + }, + { + name: "ProtonMail", + url: "https://mail.protonmail.com/", + hosts: ["protonmail.com", "protonmail.ch", "pm.me"], + }, + { + name: "Apple iCloud", + url: "https://www.icloud.com/mail/", + hosts: ["icloud.com", "me.com", "mac.com"], + }, + { + name: "Mail.ru", + url: "https://mail.ru/", + hosts: ["mail.ru", "bk.ru", "inbox.ru", "list.ru"], + }, + { + name: "Mail.ru", + url: "https://mail.ru/", + hosts: ["mail.ru", "bk.ru", "inbox.ru", "list.ru"], + }, + { + name: "AOL", + url: "https://aol.com/", + hosts: ["aol.com"], + }, + { + name: "Zoho", + url: "https://mail.zoho.com/", + hosts: ["zohomail.com", "zoho.com"], + }, + { + name: "BOL", + url: "https://email.bol.uol.com.br/", + hosts: ["bol.com.br"], + }, + { + name: "UOL", + url: "https://email.uol.com.br/", + hosts: ["uol.com.br"], + }, +]; + +export function getEmailProvider(email: string): EmailProvider { + const [, host] = email.split("@"); + + return ( + providers.find((provider) => provider.hosts.includes(host)) ?? { + name: "", + url: "", + hosts: [], + } + ); +} diff --git a/src/core/utils/upload.ts b/src/core/utils/upload.ts index 71c81bc..3caa38d 100644 --- a/src/core/utils/upload.ts +++ b/src/core/utils/upload.ts @@ -1,6 +1,6 @@ import uniqid from "uniqid"; -export async function resizeImage(file: File) { +export async function resizeImage(file: File | Blob) { const reduce = require("image-blob-reduce")(); const blob = new Blob([file], { type: "image/jpeg" }); const resizedBlob = await reduce.toBlob(blob, { max: 1024 }); @@ -8,3 +8,8 @@ export async function resizeImage(file: File) { return resizedFile; } + +export const createPreviewMedia = (media: File | Blob) => + Object.assign(media, { + preview: URL.createObjectURL(media), + }); diff --git a/src/pages/api/projects/[id]/predictions/index.ts b/src/pages/api/projects/[id]/predictions/index.ts index 052f07c..492edd6 100644 --- a/src/pages/api/projects/[id]/predictions/index.ts +++ b/src/pages/api/projects/[id]/predictions/index.ts @@ -7,6 +7,7 @@ import { getSession } from "next-auth/react"; const handler = async (req: NextApiRequest, res: NextApiResponse) => { const prompt = req.body.prompt as string; const seed = req.body.seed as number; + const image = req.body.image as string; const projectId = req.query.id as string; const session = await getSession({ req }); @@ -29,6 +30,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => { input: { prompt: replacePromptToken(prompt, project), negative_prompt: process.env.REPLICATE_NEGATIVE_PROMPT, + ...(image && { image }), ...(seed && { seed }), }, version: project.modelVersionId, diff --git a/src/pages/login.tsx b/src/pages/login.tsx index 82b6efe..9e1c6a8 100644 --- a/src/pages/login.tsx +++ b/src/pages/login.tsx @@ -1,28 +1,10 @@ import AuthForm from "@/components/layout/AuthForm"; -import { Box, Flex, Heading, Icon, Text } from "@chakra-ui/react"; -import { useRouter } from "next/router"; -import React from "react"; -import { MdCheckCircleOutline } from "react-icons/md"; +import { Flex } from "@chakra-ui/react"; -const Login = () => { - const router = useRouter(); - - return ( - - {router.query.verifyRequest ? ( - - - Check your email - - - A sign in link has been sent to your email address. - - - ) : ( - - )} - - ); -}; +const Login = () => ( + + + +); export default Login; diff --git a/src/pages/studio/[id].tsx b/src/pages/studio/[id].tsx index d47c93f..2d07e72 100644 --- a/src/pages/studio/[id].tsx +++ b/src/pages/studio/[id].tsx @@ -2,6 +2,7 @@ import PageContainer from "@/components/layout/PageContainer"; import PromptPanel from "@/components/projects/PromptPanel"; import ShotsList from "@/components/projects/shot/ShotsList"; import ProjectProvider, { PROJECTS_PER_PAGE } from "@/contexts/project-context"; +import replicateClient from "@/core/clients/replicate"; import db from "@/core/db"; import { Box, Button } from "@chakra-ui/react"; import { Project, Shot } from "@prisma/client"; @@ -17,9 +18,10 @@ export type ProjectWithShots = Project & { export interface IStudioPageProps { project: ProjectWithShots & { _count: { shots: number } }; + hasImageInputAvailable: boolean; } -const StudioPage = ({ project }: IStudioPageProps) => ( +const StudioPage = ({ project, hasImageInputAvailable }: IStudioPageProps) => ( @@ -33,7 +35,7 @@ const StudioPage = ({ project }: IStudioPageProps) => ( Back to Dashboard - + @@ -73,11 +75,20 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { }; } + const { data: model } = await replicateClient.get( + `https://api.replicate.com/v1/models/${process.env.REPLICATE_USERNAME}/${project.id}/versions/${project.modelVersionId}` + ); + + const hasImageInputAvailable = Boolean( + model.openapi_schema?.components?.schemas?.Input?.properties?.image?.title + ); + const { json: serializedProject } = superjson.serialize(project); return { props: { project: serializedProject, + hasImageInputAvailable, }, }; } diff --git a/yarn.lock b/yarn.lock index 4e6d079..5b8e2dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2333,6 +2333,13 @@ acorn@^8.8.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== +advanced-cropper@~0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/advanced-cropper/-/advanced-cropper-0.17.0.tgz#bfd003d9836c195905c628aca07a237a998d427d" + integrity sha512-j/V8p5MSkE5JZcw5qACR2L2OhBaoUZW6+wsJyA8qfUhNLX3AiEuBWMTI+A6gUCbol7PrEaGQEzayK4qcuLLyMQ== + dependencies: + tslib "^2.4.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -2603,6 +2610,11 @@ chownr@^1.1.1: resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== +classnames@^2.2.6: + version "2.3.2" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" + integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== + client-only@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" @@ -4397,6 +4409,15 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-advanced-cropper@^0.17.0: + version "0.17.0" + resolved "https://registry.yarnpkg.com/react-advanced-cropper/-/react-advanced-cropper-0.17.0.tgz#f422fa6bbee340970a023459fae4947b0aed0cd3" + integrity sha512-OMvAzGuu7V0Txe5ljePIGESc846YLNSyzm/F1QmhtNUb0uzJ4ijlu3TniVSCEdX1A1dbIT7lR/fVPG+jEEMSUQ== + dependencies: + advanced-cropper "~0.17.0" + classnames "^2.2.6" + tslib "^2.4.0" + react-clientside-effect@^1.2.6: version "1.2.6" resolved "https://registry.yarnpkg.com/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz#29f9b14e944a376b03fb650eed2a754dd128ea3a"