diff --git a/web/.gitignore b/web/.gitignore index 32d4a19e..06749214 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -2,4 +2,5 @@ .next node_modules out -tsconfig.tsbuildinfo \ No newline at end of file +tsconfig.tsbuildinfo +.env* diff --git a/web/components/editor.tsx b/web/components/editor.tsx index 4b42c6be..8de39b70 100644 --- a/web/components/editor.tsx +++ b/web/components/editor.tsx @@ -18,7 +18,6 @@ import { useCallback, useEffect, useState } from 'react' import { Linkify } from './linkify' import { uploadImage } from 'web/lib/firebase/storage' import { useMutation } from 'react-query' -import { FileUploadButton } from './file-upload-button' import { linkClass } from './site-link' import { DisplayMention } from './editor/mention' import { DisplayContractMention } from './editor/contract-mention' @@ -43,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' import { storageStore, usePersistentState, @@ -255,6 +255,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) @@ -268,12 +269,19 @@ export function TextEditor(props: { {/* Toolbar, with buttons for images and embeds */}
- setImageOpen(true)} className="-m-2.5 flex h-10 w-10 items-center justify-center rounded-full text-gray-400 hover:text-gray-500" > + + + + {isDreaming && ( +
This may take ~10 seconds...
+ )} + {/* TODO: Allow the user to choose their own modifiers */} +
Modifiers: {MODIFIERS}
+ + {/* Show the current imageUrl */} + {/* TODO: Keep the other generated images, so the user can play with different attempts. */} + {imageUrl && ( + <> + Image + + + + + + )} + + ) +} diff --git a/web/package.json b/web/package.json index f64b79e8..5f39d174 100644 --- a/web/package.json +++ b/web/package.json @@ -63,6 +63,7 @@ "react-masonry-css": "1.0.16", "react-query": "3.39.0", "react-twitter-embed": "4.0.4", + "stability-client": "1.5.0", "string-similarity": "^4.0.4", "tippy.js": "6.3.7" }, diff --git a/web/pages/api/v0/dream.ts b/web/pages/api/v0/dream.ts new file mode 100644 index 00000000..f9a0c930 --- /dev/null +++ b/web/pages/api/v0/dream.ts @@ -0,0 +1,90 @@ +import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage' +import { nanoid } from 'nanoid' +import { NextApiRequest, NextApiResponse } from 'next' +import { generateAsync } from 'stability-client' +import { storage } from 'web/lib/firebase/init' +import { + CORS_ORIGIN_MANIFOLD, + CORS_ORIGIN_LOCALHOST, +} from 'common/envs/constants' +import { applyCorsHeaders } from 'web/lib/api/cors' + +export const config = { api: { bodyParser: true } } + +// Highly experimental. Proxy for https://github.com/vpzomtrrfrt/stability-client +export default async function route(req: NextApiRequest, res: NextApiResponse) { + await applyCorsHeaders(req, res, { + origin: [CORS_ORIGIN_MANIFOLD, CORS_ORIGIN_LOCALHOST], + methods: 'POST', + }) + + // const body = JSON.parse(req.body) + // Check that prompt and apiKey are included in the body + if (!req.body.prompt) { + res.status(400).json({ message: 'Missing prompt' }) + return + } + if (!req.body.apiKey) { + res.status(400).json({ message: 'Missing apiKey' }) + return + } + /** Optional params: + outDir: string + debug: boolean + requestId: string + samples: number + engine: string + host: string + seed: number + width: number + height: number + diffusion: keyof typeof diffusionMap + steps: number + cfgScale: number + noStore: boolean + imagePrompt: {mime: string; content: Buffer} | null + stepSchedule: {start?: number; end?: number} + */ + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { _dreamResponse, images } = await generateAsync({ + ...req.body, + // Don't actually write to disk, because we're going to upload it to Firestore + noStore: true, + }) + const buffer: Buffer = images[0].buffer + const url = await upload(buffer) + + res.status(200).json({ url }) + } catch (e) { + res.status(500).json({ message: `Error running code: ${e}` }) + } +} + +// Loosely copied from web/lib/firebase/storage.ts +const ONE_YEAR_SECS = 60 * 60 * 24 * 365 + +async function upload(buffer: Buffer) { + const filename = `${nanoid(10)}.png` + const storageRef = ref(storage, `dream/${filename}`) + + function promisifiedUploadBytes(...args: any[]) { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const task = uploadBytesResumable(...args) + task.on( + 'state_changed', + null, + (e: Error) => reject(e), + () => getDownloadURL(task.snapshot.ref).then(resolve) + ) + }) + } + return promisifiedUploadBytes(storageRef, buffer, { + cacheControl: `public, max-age=${ONE_YEAR_SECS}`, + contentType: 'image/png', + }) +}