Allow users to dream directly from within Manifold

This commit is contained in:
Austin Chen 2022-10-04 21:17:20 -04:00
parent c2fa9cb4c4
commit 6f65b3854f
3 changed files with 174 additions and 4 deletions

3
web/.gitignore vendored
View File

@ -2,4 +2,5 @@
.next
node_modules
out
tsconfig.tsbuildinfo
tsconfig.tsbuildinfo
.env*

View File

@ -42,6 +42,7 @@ import ItalicIcon from 'web/lib/icons/italic-icon'
import LinkIcon from 'web/lib/icons/link-icon'
import { getUrl } from 'common/util/parse'
import { TiptapSpoiler } from 'common/util/tiptap-spoiler'
import { ImageModal } from './editor/image-modal'
const DisplayImage = Image.configure({
HTMLAttributes: {
@ -232,6 +233,7 @@ export function TextEditor(props: {
children?: React.ReactNode // additional toolbar buttons
}) {
const { editor, upload, children } = props
const [imageOpen, setImageOpen] = useState(false)
const [iframeOpen, setIframeOpen] = useState(false)
const [marketOpen, setMarketOpen] = useState(false)
@ -245,12 +247,19 @@ export function TextEditor(props: {
{/* Toolbar, with buttons for images and embeds */}
<div className="flex h-9 items-center gap-5 pl-4 pr-1">
<Tooltip text="Add image" noTap noFade>
<FileUploadButton
onFiles={upload.mutate}
<button
type="button"
onClick={() => setImageOpen(true)}
className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500"
>
<ImageModal
editor={editor}
upload={upload}
open={imageOpen}
setOpen={setImageOpen}
/>
<PhotographIcon className="h-5 w-5" aria-hidden="true" />
</FileUploadButton>
</button>
</Tooltip>
<Tooltip text="Add embed" noTap noFade>
<button

View File

@ -0,0 +1,160 @@
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: (
<Col>
<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>
</Col>
),
},
{
title: 'Dream',
content: <DreamTab {...props} />,
},
]}
/>
</Col>
</Modal>
)
}
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 url = `/api/v0/dream`
const data = {
prompt: input + ', ' + MODIFIERS,
apiKey: API_KEY,
}
const headers = {
'api-key': 'quickstart-QUdJIGlzIGNvbWluZy4uLi4K',
}
const response = await fetch(url, {
method: 'POST',
headers,
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>
)}
<div className="pt-2 text-sm text-gray-400">Modifiers: {MODIFIERS}</div>
{/* Show the current imageUrl */}
{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>
)
}