├── .env.example ├── .eslintrc.json ├── .github └── images │ └── preview.gif ├── .gitignore ├── .nvmrc ├── .prettierrc ├── README.md ├── bun.lockb ├── next.config.js ├── package.json ├── postcss.config.js ├── public ├── desktop-background.png ├── mobile-background.png └── painting-frame.png ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── app │ ├── (image-generation) │ │ ├── actions.ts │ │ └── generate-image │ │ │ ├── _components │ │ │ ├── CopyButton.tsx │ │ │ ├── DownloadButton.tsx │ │ │ ├── GenerateImageForm.tsx │ │ │ ├── ImageResult.tsx │ │ │ └── PromptForm.tsx │ │ │ ├── page.tsx │ │ │ └── result │ │ │ └── [id] │ │ │ └── page.tsx │ ├── (landing-page) │ │ ├── _components │ │ │ └── AuthButton.tsx │ │ ├── callback │ │ │ └── route.ts │ │ └── page.tsx │ ├── api │ │ └── webhook │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── lib │ │ ├── auth.ts │ │ ├── className.ts │ │ ├── envSchema.ts │ │ ├── fonts.ts │ │ ├── hooks │ │ │ ├── useEnterSubmit.ts │ │ │ ├── useMediaQuery.ts │ │ │ ├── usePrefersReducedMotion.ts │ │ │ └── useRandomInterval.ts │ │ ├── nanoid.ts │ │ ├── numbers.ts │ │ └── rateLimiter.ts │ ├── sitemap.ts │ └── ui │ │ ├── Button │ │ ├── index.tsx │ │ └── styles.module.css │ │ ├── Icons │ │ ├── ArrowLeftIcon.tsx │ │ ├── BuyMeACoffeeIcon.tsx │ │ ├── CopyIcon.tsx │ │ ├── DownloadIcon.tsx │ │ ├── LoadingIcon.tsx │ │ ├── SendIcon.tsx │ │ ├── SparkleIcon.tsx │ │ └── UploadIcon.tsx │ │ └── Sparkles │ │ ├── index.tsx │ │ └── styles.module.css └── middleware.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Authentication 2 | WORKOS_API_KEY= 3 | WORKOS_CLIENT_ID= 4 | WORKOS_REDIRECT_URI= 5 | JWT_SECRET_KEY= 6 | 7 | # Replicate 8 | REPLICATE_API_TOKEN= 9 | REPLICATE_WEBHOOK_SECRET= 10 | 11 | # Vercel's KV - Redis database 12 | KV_REST_API_READ_ONLY_TOKEN= 13 | KV_REST_API_TOKEN= 14 | KV_REST_API_URL= 15 | KV_URL= 16 | 17 | # Vercel's Blob - For static assets storage 18 | BLOB_READ_WRITE_TOKEN= 19 | 20 | # Ngrok tunnel to receive Replicate's webhook while developing locally 21 | NGROK_URL= 22 | 23 | # Sentry DSN for error reporting 24 | NEXT_PUBLIC_SENTRY_DSN= 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["import"], 3 | "rules": { 4 | "import/order": [ 5 | "error", 6 | { 7 | "groups": [ 8 | ["builtin", "external"], 9 | ["internal", "parent", "sibling", "index"] 10 | ], 11 | "newlines-between": "always", 12 | "alphabetize": { 13 | "order": "asc", 14 | "caseInsensitive": true 15 | } 16 | } 17 | ] 18 | }, 19 | "extends": "next/core-web-vitals" 20 | } 21 | -------------------------------------------------------------------------------- /.github/images/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/starry/ba77a7ac2996e143ad0afa2fc4253f9686dcfb6d/.github/images/preview.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | # Sentry Config File 38 | .sentryclirc 39 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18.17.0 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "quoteProps": "consistent", 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "plugins": ["prettier-plugin-tailwindcss"] 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Starry – Generate pictures based on Van Gogh's Starry Night. 4 |

Starry 💫

5 | 6 |

7 | 8 |

9 | Turn your ideas into Van Gogh's Starry Night 10 |

11 | 12 |

13 | 14 | Laura Twitter followers count 15 | 16 | 17 | Spirals repository stars count 18 | 19 |

20 | 21 | ## How it works 22 | 23 | This application leverages an AI model, specifically [Stable Diffusion](https://replicate.com/stability-ai/stable-diffusion) hosted on Replicate, to transform your images into art inspired by Van Gogh's Starry Night. Simply enter a prompt, and it will be processed through this AI model via a Next.js server action and an API route that listens to Replicate's webhook. 24 | 25 | ## 🎥 Walkthrough videos 26 | 27 | 28 | 29 | - [Video explanation on how I built this application]() 30 | - [Launch demo video](https://x.com/lauradotjs/status/1754970380547117314?s=20) 31 | 32 | ## Powered by 33 | 34 | - [Bun](https://bun.sh/) for compilation 35 | - [Replicate](https://replicate.ai/) for AI API 36 | - [Vercel](https://vercel.com) 37 | - Next.js [App Router](https://nextjs.org/docs/app) 38 | - Next.js [Server Actions](https://nextjs.org/docs/app/api-reference/functions/server-actions) 39 | - [Vercel Blob](https://vercel.com/storage/blob) for image storage 40 | - [Vercel KV](https://vercel.com/storage/kv) for redis 41 | - [WorkOS](https://workos.com/) 42 | - [AuthKit](https://authkit.com/) for user management 43 | 44 | ## How to run on your own machine 45 | 46 | ### 1. Install [Bun](https://bun.sh/) for compilation 47 | 48 | ### 2. Install [ngrok](https://ngrok.com/) to listen to Replicate's webhook events while developing locally 49 | 50 | ### 2. Cloning the repository 51 | 52 | ```bash 53 | git clone 54 | ``` 55 | 56 | ### 3. Sign up for the services mentioned in the [Powered by](#powered-by) section 57 | 58 | ### 4. Storing API keys in .env file. 59 | 60 | Copy the `.env.example` file and update with your own API keys. 61 | 62 | ### 5. Installing the dependencies. 63 | 64 | ```bash 65 | bun install 66 | ``` 67 | 68 | ### 6. Running the application. 69 | 70 | Then, run the application in the command line and it will be available at `http://localhost:3000`. 71 | 72 | ```bash 73 | bun run dev 74 | ``` 75 | 76 | Also, run `ngrok http 3000` to create a tunnel to listen to Replicate's webhook events. 77 | 78 | ## Author 79 | 80 | - Laura Beatris ([@lauradotjs](https://twitter.com/lauradotjs)) 81 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/starry/ba77a7ac2996e143ad0afa2fc4253f9686dcfb6d/bun.lockb -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: 'https', 7 | hostname: 'jxgoqlyxc3jqoq07.public.blob.vercel-storage.com', 8 | port: '', 9 | }, 10 | ], 11 | }, 12 | async redirects() { 13 | return [ 14 | { 15 | source: '/github', 16 | destination: 'https://github.com/laurabeatris/starry', 17 | permanent: false, 18 | }, 19 | ]; 20 | }, 21 | }; 22 | 23 | module.exports = nextConfig; 24 | 25 | const { withSentryConfig } = require('@sentry/nextjs'); 26 | 27 | module.exports = withSentryConfig( 28 | module.exports, 29 | { 30 | // For all available options, see: 31 | // https://github.com/getsentry/sentry-webpack-plugin#options 32 | 33 | // Suppresses source map uploading logs during build 34 | silent: true, 35 | org: 'personal-9x3', 36 | project: 'starry', 37 | }, 38 | { 39 | // For all available options, see: 40 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 41 | 42 | // Upload a larger set of source maps for prettier stack traces (increases build time) 43 | widenClientFileUpload: true, 44 | 45 | // Transpiles SDK to be compatible with IE11 (increases bundle size) 46 | transpileClientSDK: true, 47 | 48 | // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) 49 | tunnelRoute: '/monitoring', 50 | 51 | // Hides source maps from generated client bundles 52 | hideSourceMaps: true, 53 | 54 | // Automatically tree-shake Sentry logger statements to reduce bundle size 55 | disableLogger: true, 56 | 57 | // Enables automatic instrumentation of Vercel Cron Monitors. 58 | // See the following for more information: 59 | // https://docs.sentry.io/product/crons/ 60 | // https://vercel.com/docs/cron-jobs 61 | automaticVercelMonitors: true, 62 | }, 63 | ); 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "starry", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --ignore-path .gitignore --write ." 11 | }, 12 | "dependencies": { 13 | "@sentry/nextjs": "^7.99.0", 14 | "@upstash/ratelimit": "^1.0.0", 15 | "@vercel/analytics": "^1.1.2", 16 | "@vercel/blob": "^0.19.0", 17 | "@vercel/kv": "^1.0.1", 18 | "@vercel/speed-insights": "^1.0.7", 19 | "@workos-inc/node": "^4.0.0", 20 | "clsx": "^2.0.0", 21 | "fp-ts": "^2.16.2", 22 | "jose": "^5.2.0", 23 | "next": "canary", 24 | "promptmaker": "^1.1.0", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "replicate": "^0.25.2", 28 | "tailwind-merge": "^1.14.0", 29 | "tailwindcss": "3.3.3", 30 | "zod": "^3.22.4" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "20.6.5", 34 | "@types/react": "^18.2.48", 35 | "@types/react-dom": "^18.2.18", 36 | "autoprefixer": "10.4.16", 37 | "eslint": "8.50.0", 38 | "eslint-config-next": "13.5.2", 39 | "eslint-plugin-import": "^2.29.1", 40 | "postcss": "8.4.30", 41 | "prettier": "^3.1.1", 42 | "prettier-plugin-tailwindcss": "^0.5.11", 43 | "typescript": "5.2.2" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/desktop-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/starry/ba77a7ac2996e143ad0afa2fc4253f9686dcfb6d/public/desktop-background.png -------------------------------------------------------------------------------- /public/mobile-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/starry/ba77a7ac2996e143ad0afa2fc4253f9686dcfb6d/public/mobile-background.png -------------------------------------------------------------------------------- /public/painting-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LauraBeatris/starry/ba77a7ac2996e143ad0afa2fc4253f9686dcfb6d/public/painting-frame.png -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | 16 | replaysOnErrorSampleRate: 1.0, 17 | 18 | // This sets the sample rate to be 10%. You may want this to be 100% while 19 | // in development and sample at a lower rate in production 20 | replaysSessionSampleRate: 0.1, 21 | 22 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 23 | integrations: [ 24 | Sentry.replayIntegration({ 25 | // Additional Replay configuration goes in here, for example: 26 | maskAllText: true, 27 | blockAllMedia: true, 28 | }), 29 | ], 30 | }); 31 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from '@sentry/nextjs'; 7 | 8 | Sentry.init({ 9 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/(image-generation)/actions.ts: -------------------------------------------------------------------------------- 1 | 'use server'; 2 | 3 | import { kv } from '@vercel/kv'; 4 | import * as E from 'fp-ts/lib/Either'; 5 | import { redirect } from 'next/navigation'; 6 | import Replicate from 'replicate'; 7 | import * as z from 'zod'; 8 | 9 | import { getUser } from '@/app/lib/auth'; 10 | import { nanoid } from '@/app/lib/nanoid'; 11 | import { performRateLimitByUser } from '@/app/lib/rateLimiter'; 12 | 13 | const replicate = new Replicate({ 14 | auth: process.env.REPLICATE_API_TOKEN, 15 | }); 16 | 17 | const GenerateImageFormSchema = z.object({ 18 | prompt: z.string({ 19 | invalid_type_error: 'Please enter a prompt.', 20 | }), 21 | }); 22 | 23 | export type FormState = { 24 | errors?: { 25 | prompt?: string[]; 26 | }; 27 | message?: string | null; 28 | }; 29 | 30 | export async function generateImage(_prevState: FormState, formData: FormData) { 31 | const parsedFormData = GenerateImageFormSchema.safeParse( 32 | Object.fromEntries(formData.entries()), 33 | ); 34 | 35 | if (!parsedFormData.success) { 36 | return { 37 | errors: parsedFormData.error.flatten().fieldErrors, 38 | message: 'Invalid form data. Make sure to send a prompt value.', 39 | }; 40 | } 41 | 42 | const getUserResult = await getUser(); 43 | 44 | if (E.isLeft(getUserResult)) { 45 | redirect('/'); 46 | } 47 | 48 | if (process.env.RATE_LIMIT_ENABLED) { 49 | const { user } = getUserResult.right; 50 | const rateLimitResult = await performRateLimitByUser(user); 51 | 52 | if (E.isLeft(rateLimitResult)) { 53 | return { 54 | message: rateLimitResult.left, 55 | }; 56 | } 57 | } 58 | 59 | const id = nanoid(); 60 | 61 | const { prompt } = parsedFormData.data; 62 | 63 | await Promise.all([ 64 | kv.hset(id, { 65 | prompt, 66 | }), 67 | replicate.predictions.create({ 68 | version: 69 | 'ac732df83cea7fff18b8472768c88ad041fa750ff7682a21affe81863cbe77e4', 70 | input: { 71 | prompt: `Use a background as a starry night, but with a ${prompt}`, 72 | width: 768, 73 | height: 768, 74 | scheduler: 'K_EULER', 75 | num_outputs: 1, 76 | guidance_scale: 7.5, 77 | num_inference_steps: 50, 78 | }, 79 | webhook: `${process.env.REPLICATE_WEBHOOK_URL}?id=${id}&secret=${process.env.REPLICATE_WEBHOOK_SECRET}`, 80 | webhook_events_filter: ['completed'], 81 | }), 82 | ]); 83 | 84 | redirect(`/generate-image/result/${id}`); 85 | } 86 | -------------------------------------------------------------------------------- /src/app/(image-generation)/generate-image/_components/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import analytics from '@vercel/analytics'; 4 | import { useState } from 'react'; 5 | 6 | import { CopyIcon } from '@/app/ui/Icons/CopyIcon'; 7 | import { LoadingCircleIcon } from '@/app/ui/Icons/LoadingIcon'; 8 | 9 | interface CopyButtonProps { 10 | generatedImageUrl: string; 11 | imageId: string; 12 | } 13 | 14 | export function CopyButton({ generatedImageUrl, imageId }: CopyButtonProps) { 15 | const [isCopying, setIsCopying] = useState(false); 16 | 17 | function copyImage() { 18 | setIsCopying(true); 19 | 20 | analytics.track('Copy image', { 21 | generatedImageUrl, 22 | page: `/generate-image/result/${imageId}`, 23 | }); 24 | 25 | fetch(generatedImageUrl, { 26 | headers: new Headers({ 27 | Origin: location.origin, 28 | }), 29 | mode: 'cors', 30 | }) 31 | .then((response) => response.blob()) 32 | .then((blob) => { 33 | navigator.clipboard.write([ 34 | new ClipboardItem({ 35 | 'image/png': blob, 36 | }), 37 | ]); 38 | }) 39 | .catch((e) => console.error(e)) 40 | .finally(() => setIsCopying(false)); 41 | } 42 | 43 | return ( 44 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/app/(image-generation)/generate-image/_components/DownloadButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import analytics from '@vercel/analytics'; 4 | import { useState } from 'react'; 5 | 6 | import { DownloadIcon } from '@/app/ui/Icons/DownloadIcon'; 7 | import { LoadingCircleIcon } from '@/app/ui/Icons/LoadingIcon'; 8 | 9 | interface DownloadButtonProps { 10 | generatedImageUrl: string; 11 | imageId: string; 12 | } 13 | 14 | export function DownloadButton({ 15 | generatedImageUrl, 16 | imageId, 17 | }: DownloadButtonProps) { 18 | const [isDownloading, setIsDownloading] = useState(false); 19 | 20 | function downloadImage() { 21 | setIsDownloading(true); 22 | analytics.track('Download image', { 23 | generatedImageUrl, 24 | page: `/generate-image/result/${imageId}`, 25 | }); 26 | 27 | fetch(generatedImageUrl, { 28 | headers: new Headers({ 29 | Origin: location.origin, 30 | }), 31 | mode: 'cors', 32 | }) 33 | .then((response) => response.blob()) 34 | .then((blob) => { 35 | let blobUrl = window.URL.createObjectURL(blob); 36 | 37 | const a = document.createElement('a'); 38 | a.download = `${imageId || 'generated-image'}.png`; 39 | a.href = blobUrl; 40 | document.body.appendChild(a); 41 | a.click(); 42 | a.remove(); 43 | }) 44 | .catch((e) => console.error(e)) 45 | .finally(() => setIsDownloading(false)); 46 | } 47 | 48 | return ( 49 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/app/(image-generation)/generate-image/_components/GenerateImageForm.tsx: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import promptmaker from 'promptmaker'; 3 | 4 | import { PromptForm } from './PromptForm'; 5 | 6 | type GenerateImageFormProps = { 7 | imagePlaceholder?: string; 8 | remainingGenerations: number; 9 | }; 10 | 11 | export function GenerateImageForm({ 12 | remainingGenerations, 13 | }: GenerateImageFormProps) { 14 | return ( 15 |
16 | 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/(image-generation)/generate-image/_components/ImageResult.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useEffect } from 'react'; 6 | 7 | import { LoadingCircleIcon } from '@/app/ui/Icons/LoadingIcon'; 8 | 9 | interface ImageResultProps { 10 | generatedImageUrl?: string; 11 | } 12 | 13 | const intervalMilliseconds = 1000; 14 | 15 | export function ImageResult({ generatedImageUrl }: ImageResultProps) { 16 | const router = useRouter(); 17 | 18 | /** 19 | * Interval to keep refreshing/pooling the page until the image is 20 | * stored on Vercel blob storage 21 | */ 22 | useEffect(() => { 23 | let interval: NodeJS.Timeout; 24 | 25 | if (!generatedImageUrl) { 26 | interval = setInterval(() => { 27 | router.refresh(); 28 | }, intervalMilliseconds); 29 | } 30 | 31 | return () => clearInterval(interval); 32 | }, [generatedImageUrl, router]); 33 | 34 | return ( 35 |
36 | {generatedImageUrl ? ( 37 | Generated image 44 | ) : ( 45 | <> 46 | 47 |

48 | Your image is currently being created.
This usually takes 49 | about 20 seconds.{' '} 50 |

51 | 52 | )} 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/app/(image-generation)/generate-image/_components/PromptForm.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import analytics from '@vercel/analytics'; 4 | // @ts-ignore 5 | import { ElementRef, useEffect, useRef, useState } from 'react'; 6 | import { useFormState, useFormStatus } from 'react-dom'; 7 | 8 | import { generateImage } from '@/app/(image-generation)/actions'; 9 | import { className } from '@/app/lib/className'; 10 | import useEnterSubmit from '@/app/lib/hooks/useEnterSubmit'; 11 | import { LoadingCircleIcon } from '@/app/ui/Icons/LoadingIcon'; 12 | import { SendIcon } from '@/app/ui/Icons/SendIcon'; 13 | 14 | declare global { 15 | interface promptmaker { 16 | (): string; 17 | } 18 | } 19 | 20 | interface PromptFormProps { 21 | initialPromptText?: string; 22 | disabled?: boolean; 23 | } 24 | 25 | const initialFormState = { message: '', errors: {} }; 26 | 27 | export function PromptForm({ disabled, initialPromptText }: PromptFormProps) { 28 | const [prompt, setPrompt] = useState(''); 29 | const [placeholderPrompt, setPlaceholderPrompt] = useState(initialPromptText); 30 | 31 | const [state, dispatch] = useFormState(generateImage, initialFormState); 32 | 33 | const textareaRef = useRef>(null); 34 | 35 | const { formRef, onKeyDown } = useEnterSubmit(); 36 | 37 | useEffect(() => { 38 | if (textareaRef.current) { 39 | textareaRef.current.select(); 40 | } 41 | }, [initialPromptText]); 42 | 43 | return ( 44 | <> 45 |
{ 49 | analytics.track('Generating image with prompt', { 50 | prompt: prompt, 51 | }); 52 | dispatch(data); 53 | }} 54 | > 55 |