manifold/web/components/editor/image-modal.tsx
Austin Chen cee8caa3e8
Generate images from StableDiffusion (#1035)
* Generate images from StableDiffusion

* Update yarn.lock

* Log an error, remove extra comment

* Code cleanup

* Note about the API
2022-10-12 09:53:04 -07:00

158 lines
4.7 KiB
TypeScript

import { UploadIcon } from '@heroicons/react/outline'
import { Editor } from '@tiptap/react'
import { useState } from 'react'
import { AlertBox } from '../alert-box'
import { Button } from '../button'
import { FileUploadButton } from '../file-upload-button'
import { Col } from '../layout/col'
import { Modal } from '../layout/modal'
import { Row } from '../layout/row'
import { Tabs } from '../layout/tabs'
const MODIFIERS =
'8k, beautiful, illustration, trending on art station, picture of the day, epic composition'
export function ImageModal(props: {
editor: Editor | null
// TODO: Type this correctly?
upload: any
open: boolean
setOpen: (open: boolean) => void
}) {
const { upload, open, setOpen } = props
return (
<Modal open={open} setOpen={setOpen}>
<Col className="gap-2 rounded bg-white p-6">
<Tabs
tabs={[
{
title: 'Upload file',
content: (
<FileUploadButton
onFiles={(files) => {
setOpen(false)
upload.mutate(files)
}}
className="relative block w-full rounded-lg border-2 border-dashed border-gray-300 p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
>
<UploadIcon className="mx-auto h-12 w-12 text-gray-400" />
<span className="mt-2 block text-sm font-medium text-gray-400">
Upload an image file
</span>
</FileUploadButton>
),
},
{
title: 'Dream',
content: <DreamTab {...props} />,
},
]}
/>
</Col>
</Modal>
)
}
// Note: this is currently tied to a DreamStudio API key tied to akrolsmir@gmail.com,
// and injected on Vercel.
const API_KEY = process.env.NEXT_PUBLIC_DREAM_KEY
function DreamTab(props: {
editor: Editor | null
open: boolean
setOpen: (open: boolean) => void
}) {
const { editor, setOpen } = props
const [input, setInput] = useState('')
const [isDreaming, setIsDreaming] = useState(false)
const [imageUrl, setImageUrl] = useState('')
const imageCode = `<img src="${imageUrl}" alt="${input}" />`
if (!API_KEY) {
return (
<AlertBox
title="Missing API Key"
text="An API key from https://beta.dreamstudio.ai/ is needed to dream; add it to your web/.env.local"
/>
)
}
async function dream() {
setIsDreaming(true)
const data = {
prompt: input + ', ' + MODIFIERS,
apiKey: API_KEY,
}
const response = await fetch(`/api/v0/dream`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
})
const json = await response.json()
setImageUrl(json.url)
setIsDreaming(false)
}
return (
<Col className="gap-2">
<Row className="gap-2">
<input
autoFocus
type="text"
name="embed"
id="embed"
className="block w-full rounded-md border-gray-300 shadow-sm placeholder:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
placeholder="A crane playing poker on a green table"
value={input}
onChange={(e) => setInput(e.target.value)}
autoComplete="off"
/>
<Button
className="whitespace-nowrap"
onClick={dream}
loading={isDreaming}
>
Dream
{/* TODO: Charge M$5 with ({formatMoney(5)}) */}
</Button>
</Row>
{isDreaming && (
<div className="text-sm">This may take ~10 seconds...</div>
)}
{/* TODO: Allow the user to choose their own modifiers */}
<div className="pt-2 text-sm text-gray-400">Modifiers: {MODIFIERS}</div>
{/* Show the current imageUrl */}
{/* TODO: Keep the other generated images, so the user can play with different attempts. */}
{imageUrl && (
<>
<img src={imageUrl} alt="Image" />
<Row className="gap-2">
<Button
disabled={isDreaming}
onClick={() => {
if (editor) {
editor.chain().insertContent(imageCode).run()
setInput('')
setOpen(false)
}
}}
>
Add image
</Button>
<Button
color="gray"
onClick={() => {
setInput('')
setOpen(false)
}}
>
Cancel
</Button>
</Row>
</>
)}
</Col>
)
}