158 lines
4.7 KiB
TypeScript
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>
|
||
|
)
|
||
|
}
|