diff options
Diffstat (limited to 'src/client/views/smartdraw')
| -rw-r--r-- | src/client/views/smartdraw/AnnotationPalette.tsx | 361 | ||||
| -rw-r--r-- | src/client/views/smartdraw/DrawingFillHandler.tsx | 79 | ||||
| -rw-r--r-- | src/client/views/smartdraw/FireflyConstants.ts | 49 | ||||
| -rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.scss | 87 | ||||
| -rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.tsx | 424 | ||||
| -rw-r--r-- | src/client/views/smartdraw/StickerPalette.scss (renamed from src/client/views/smartdraw/AnnotationPalette.scss) | 2 | ||||
| -rw-r--r-- | src/client/views/smartdraw/StickerPalette.tsx | 352 |
7 files changed, 812 insertions, 542 deletions
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx deleted file mode 100644 index f1e2e4f41..000000000 --- a/src/client/views/smartdraw/AnnotationPalette.tsx +++ /dev/null @@ -1,361 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Slider, Switch } from '@mui/material'; -import { Button } from 'browndash-components'; -import { action, makeObservable, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { AiOutlineSend } from 'react-icons/ai'; -import ReactLoading from 'react-loading'; -import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; -import { ImageCast, NumCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; -import { DocumentType } from '../../documents/DocumentTypes'; -import { Docs } from '../../documents/Documents'; -import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; -import { SettingsManager } from '../../util/SettingsManager'; -import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; -import { ObservableReactComponent } from '../ObservableReactComponent'; -import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; -import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; -import { FieldView } from '../nodes/FieldView'; -import './AnnotationPalette.scss'; -import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; - -interface AnnotationPaletteProps { - Document: Doc; -} - -/** - * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette - * is to offer an easy way for users to save then drag and drop repeated annotations onto a document. - * These annotations can be of any annotation type and operate similarly to user templates. - * - * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can - * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing - * to choose from. These drawings can then be saved to the palette as annotations. - */ -@observer -export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> { - @observable private _paletteMode: 'create' | 'view' = 'view'; - @observable private _userInput: string = ''; - @observable private _isLoading: boolean = false; - @observable private _canInteract: boolean = true; - @observable private _showRegenerate: boolean = false; - @observable private _docView: DocumentView | null = null; - @observable private _docCarouselView: DocumentView | null = null; - @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; - private _gptRes: string[] = []; - - constructor(props: AnnotationPaletteProps) { - super(props); - makeObservable(this); - } - - public static LayoutString(fieldKey: string) { - return FieldView.LayoutString(AnnotationPalette, fieldKey); - } - - Contains = (view: DocumentView) => { - return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); - }; - - return170 = () => 170; - - @action - handleKeyPress = async (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - await this.generateDrawings(); - } - }; - - @action - setPaletteMode = (mode: 'create' | 'view') => { - this._paletteMode = mode; - }; - - @action - setUserInput = (input: string) => { - if (!this._isLoading) this._userInput = input; - }; - - @action - setDetail = (detail: number) => { - if (this._canInteract) this._opts.complexity = detail; - }; - - @action - setColor = (autoColor: boolean) => { - if (this._canInteract) this._opts.autoColor = autoColor; - }; - - @action - setSize = (size: number) => { - if (this._canInteract) this._opts.size = size; - }; - - @action - resetPalette = (changePaletteMode: boolean) => { - if (changePaletteMode) this.setPaletteMode('view'); - this.setUserInput(''); - this.setDetail(5); - this.setColor(true); - this.setSize(200); - this._showRegenerate = false; - this._canInteract = true; - this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; - this._gptRes = []; - this._props.Document[DocData].data = undefined; - }; - - /** - * Adds a doc to the annotation palette. Gets a snapshot of the document to use as a preview in the palette. When this - * preview is dragged onto a parent document, a copy of that document is added as an annotation. - */ - public static addToPalette = async (doc: Doc) => { - if (!doc.savedAsAnno) { - const docView = DocumentView.getDocumentView(doc); - await docView?.ComponentView?.updateIcon?.(true); - const { clone } = await Doc.MakeClone(doc); - clone.title = doc.title; - const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; - Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image)); - doc.savedAsAnno = true; - } - }; - - public static getIcon(group: Doc) { - const docView = DocumentView.getDocumentView(group); - if (docView) { - docView.ComponentView?.updateIcon?.(true); - return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); - } - return undefined; - } - - /** - * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from - * the annotation palette. - */ - @undoBatch - generateDrawings = action(async () => { - this._isLoading = true; - this._props.Document[DocData].data = undefined; - for (let i = 0; i < 3; i++) { - try { - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - this._canInteract = false; - if (this._showRegenerate) { - await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput); - } else { - await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor); - } - } catch (e) { - console.log('Error generating drawing', e); - } - } - this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); - this._userInput = ''; - this._isLoading = false; - this._showRegenerate = true; - }); - - @action - addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => { - this._gptRes.push(gptRes); - drawing[DocData].freeform_fitContentsToBox = true; - Doc.AddDocToList(this._props.Document, 'data', drawing); - }; - - /** - * Saves the currently showing, newly generated drawing to the annotation palette and sets the metadata. - * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user - * presses the "save drawing" button. - */ - saveDrawing = async () => { - const cIndex = NumCast(this._props.Document.carousel_index); - const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; - const docData = focusedDrawing[DocData]; - docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text; - docData.drawingInput = this._opts.text; - docData.drawingComplexity = this._opts.complexity; - docData.drawingColored = this._opts.autoColor; - docData.drawingSize = this._opts.size; - docData.drawingData = this._gptRes[cIndex]; - docData.width = this._opts.size; - docData.x = this._opts.x; - docData.y = this._opts.y; - await AnnotationPalette.addToPalette(focusedDrawing); - this.resetPalette(true); - }; - - render() { - return ( - <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}> - {this._paletteMode === 'view' && ( - <> - <DocumentView - ref={r => (this._docView = r)} - Document={Doc.MyAnnos} - addDocument={undefined} - addDocTab={DocumentViewInternal.addDocTabFunc} - pinToPres={DocumentView.PinDoc} - containerViewPath={returnEmptyDocViewList} - styleProvider={DefaultStyleProvider} - removeDocument={returnFalse} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={this.return170} - PanelHeight={this.return170} - renderDepth={0} - isContentActive={returnTrue} - focus={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> - <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} /> - </> - )} - {this._paletteMode === 'create' && ( - <> - <div className="palette-create"> - <input - className="palette-create-input" - aria-label="label-input" - id="new-label" - type="text" - value={this._userInput} - onChange={e => { - this.setUserInput(e.target.value); - }} - placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'} - onKeyDown={this.handleKeyPress} - /> - <Button - style={{ alignSelf: 'flex-end' }} - tooltip={this._showRegenerate ? 'Regenerate' : 'Send'} - icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />} - iconPlacement="right" - color={SettingsManager.userColor} - onClick={this.generateDrawings} - /> - </div> - <div className="palette-create-options"> - <div className="palette-color"> - Color - <Switch - sx={{ - '& .MuiSwitch-switchBase.Mui-checked': { - color: SettingsManager.userColor, - }, - '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { - backgroundColor: SettingsManager.userVariantColor, - }, - }} - defaultChecked={true} - value={this._opts.autoColor} - size="small" - onChange={() => this.setColor(!this._opts.autoColor)} - /> - </div> - <div className="palette-detail"> - Detail - <Slider - sx={{ - '& .MuiSlider-thumb': { - color: SettingsManager.userColor, - '&.Mui-focusVisible, &:hover, &.Mui-active': { - boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`, - }, - }, - '& .MuiSlider-track': { - color: SettingsManager.userVariantColor, - }, - '& .MuiSlider-rail': { - color: SettingsManager.userColor, - }, - }} - style={{ width: '80%' }} - min={1} - max={10} - step={1} - size="small" - value={this._opts.complexity} - onChange={(e, val) => { - this.setDetail(val as number); - }} - valueLabelDisplay="auto" - /> - </div> - <div className="palette-size"> - Size - <Slider - sx={{ - '& .MuiSlider-thumb': { - color: SettingsManager.userColor, - '&.Mui-focusVisible, &:hover, &.Mui-active': { - boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`, - }, - }, - '& .MuiSlider-track': { - color: SettingsManager.userVariantColor, - }, - '& .MuiSlider-rail': { - color: SettingsManager.userColor, - }, - }} - style={{ width: '80%' }} - min={50} - max={500} - step={10} - size="small" - value={this._opts.size} - onChange={(e, val) => { - this.setSize(val as number); - }} - valueLabelDisplay="auto" - /> - </div> - </div> - <DocumentView - ref={r => (this._docCarouselView = r)} - Document={this._props.Document} - addDocument={undefined} - addDocTab={DocumentViewInternal.addDocTabFunc} - pinToPres={DocumentView.PinDoc} - containerViewPath={returnEmptyDocViewList} - styleProvider={DefaultStyleProvider} - removeDocument={returnFalse} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={this.return170} - PanelHeight={this.return170} - renderDepth={1} - isContentActive={returnTrue} - focus={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> - <div className="palette-buttons"> - <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} /> - <div className="palette-save-reset"> - <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} /> - <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} /> - </div> - </div> - </> - )} - </div> - ); - } -} - -Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { - layout: { view: AnnotationPalette, dataField: 'data' }, - options: { acl: '' }, -}); diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx new file mode 100644 index 000000000..d0a840883 --- /dev/null +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -0,0 +1,79 @@ +import { imageUrlToBase64 } from '../../../ClientUtils'; +import { Doc, StrListCast } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { DocCast, ImageCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { Upload } from '../../../server/SharedMediaTypes'; +import { gptDescribeImage } from '../../apis/gpt/GPT'; +import { Docs } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { OpenWhere } from '../nodes/OpenWhere'; +import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants'; + +const DashDropboxId = '2m86iveqdr9vzsa'; +export class DrawingFillHandler { + static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string, styleDoc?: Doc) => { + const docData = drawing[DocData]; + const tags = StrListCast(docData.tags).map(tag => tag.slice(1)); + const styles = tags.filter(tag => FireflyStylePresets.has(tag)); + const styleDocs = !Doc.Links(drawing).length + ? styleDoc && !tags.length + ? [styleDoc] + : [] + : Doc.Links(drawing) + .map(link => Doc.getOppositeAnchor(link, drawing)) + .map(anchor => anchor && DocCast(anchor.embedContainer)); + const styleUrl = await DocumentView.GetDocImage(styleDocs.filter(doc => doc?.data instanceof ImageField).lastElement())?.then(styleImg => { + const hrefParts = ImageCast(styleImg).url.href.split('.'); + return `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; + }); + DocumentView.GetDocImage(drawing)?.then(imageField => { + if (imageField) { + const aspectRatio = (drawing.width as number) / (drawing.height as number); + let dims: { width: number; height: number }; + if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) { + dims = FireflyDimensionsMap[FireflyImageDimensions.Widescreen]; + } else if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Landscape]) { + dims = FireflyDimensionsMap[FireflyImageDimensions.Landscape]; + } else if (aspectRatio < AspectRatioLimits[FireflyImageDimensions.Portrait]) { + dims = FireflyDimensionsMap[FireflyImageDimensions.Portrait]; + } else { + dims = FireflyDimensionsMap[FireflyImageDimensions.Square]; + } + const { href } = ImageCast(imageField).url; + const hrefParts = href.split('.'); + const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; + return imageUrlToBase64(structureUrl) + .then(gptDescribeImage) + .then((prompt, newPrompt = user_prompt || prompt) => + Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl }) + .then((infos: Upload.ImageInformation[]) => { + const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 }); + drawing[DocData].ai_firefly_generatedDocs = genratedDocs; + infos.map(info => + Doc.AddDocToList( + genratedDocs, + undefined, + Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { + ai: 'firefly', + title: newPrompt, + ai_firefly_prompt: newPrompt, + _width: 500, + data_nativeWidth: info.nativeWidth, + data_nativeHeight: info.nativeHeight, + }) + ) + ); + if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight); + }) + .catch(e => { + if (e.toString().includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + e.toString().replace(/^[^"]*/, ''))) { + window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus(); + } else alert(e.toString()); + }) + ); // prettier-ignore:q + } + }); + }; +} diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts new file mode 100644 index 000000000..1f1781617 --- /dev/null +++ b/src/client/views/smartdraw/FireflyConstants.ts @@ -0,0 +1,49 @@ +export interface FireflyImageData { + prompt: string; + seed: number | undefined; + pathname: string; + href?: string; +} + +export enum FireflyImageDimensions { + Square = 'square', + Landscape = 'landscape', + Portrait = 'portrait', + Widescreen = 'widescreen', +} + +export const FireflyDimensionsMap = { + square: { width: 2048, height: 2048 }, + landscape: { width: 2304, height: 1792 }, + portrait: { width: 1792, height: 2304 }, + widescreen: { width: 2688, height: 1536 }, +}; + +export const AspectRatioLimits = { + square: 1, + landscape: 1.167, + portrait: 0.875, + widescreen: 1.472, +}; + +// prettier-ignore +export const FireflyStylePresets = + new Set<string>(['graphic', 'wireframe', + 'vector_look','bw','cool_colors','golden','monochromatic','muted_color','toned_image','vibrant_colors','warm_tone','closeup', + 'knolling','landscape_photography','macrophotography','photographed_through_window','shallow_depth_of_field','shot_from_above', + 'shot_from_below','surface_detail','wide_angle','beautiful','bohemian','chaotic','dais','divine','eclectic','futuristic','kitschy', + 'nostalgic','simple','antique_photo','bioluminescent','bokeh','color_explosion','dark','faded_image','fisheye','gomori_photography', + 'grainy_film','iridescent','isometric','misty','neon','otherworldly_depiction','ultraviolet','underwater', 'backlighting', + 'dramatic_light', 'golden_hour', 'harsh_light','long','low_lighting','multiexposure','studio_light','surreal_lighting', + '3d_patterns','charcoal','claymation','fabric','fur','guilloche_patterns','layered_paper','marble_sculpture','made_of_metal', + 'origami','paper_mache','polka','strange_patterns','wood_carving','yarn','art_deco','art_nouveau','baroque','bauhaus', + 'constructivism','cubism','cyberpunk','fantasy','fauvism', 'film_noir','glitch_art','impressionism','industrialism','maximalism', + 'minimalism','modern_art','modernism','neo','pointillism','psychedelic','science_fiction','steampunk','surrealism','synthetism', + 'synthwave','vaporwave','acrylic_paint','bold_lines','chiaroscuro','color_shift_art','daguerreotype','digital_fractal', + 'doodle_drawing','double_exposure_portrait','fresco','geometric_pen','halftone','ink','light_painting','line_drawing','linocut', + 'oil_paint','paint_spattering','painting','palette_knife','photo_manipulation','scribble_texture','sketch','splattering', + 'stippling_drawing','watercolor','3d','anime','cartoon','cinematic','comic_book','concept_art','cyber_matrix','digital_art', + 'flat_design','geometric','glassmorphism','glitch_graphic','graffiti','hyper_realistic','interior_design','line_gradient', + 'low_poly','newspaper_collage','optical_illusion','pattern_pixel','pixel_art','pop_art','product_photo','psychedelic_background', + 'psychedelic_wonderland','scandinavian','splash_images','stamp','trompe_loeil' + ]); diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss index 0e8bd3349..cca7d77c7 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.scss +++ b/src/client/views/smartdraw/SmartDrawHandler.scss @@ -1,44 +1,77 @@ .smart-draw-handler { position: absolute; -} - -.smartdraw-input { - color: black; -} -.smartdraw-options { - display: flex; - flex-direction: row; - justify-content: space-around; - - .auto-color { + .smart-draw-main { display: flex; - flex-direction: column; - justify-content: center; - width: 30%; + flex-direction: row; + + .smartdraw-input { + color: black; + margin: auto; + } } - .complexity { + .smartdraw-output-options { display: flex; - flex-direction: column; + flex-direction: row; justify-content: center; - width: 31%; + margin-top: 3px; } - .size { + .smartdraw-options-container { + width: 265px; + padding: 5px; + font-weight: bolder; + text-align: center; display: flex; flex-direction: column; - justify-content: center; - width: 39%; - .size-slider { - width: 80%; + .smartdraw-options { + font-weight: normal; + margin-top: 5px; + display: flex; + flex-direction: row; + justify-content: space-around; + + .smartdraw-auto-color { + display: flex; + flex-direction: column; + align-items: center; + width: 30%; + margin-top: 3px; + } + + .smartdraw-complexity { + display: flex; + flex-direction: column; + align-items: center; + width: 31%; + margin-top: 3px; + } + + .smartdraw-size { + display: flex; + flex-direction: column; + align-items: center; + width: 39%; + margin-top: 3px; + } + } + + .smartdraw-dimensions { + font-weight: normal; + margin-top: 7px; + padding-left: 25px; } } -} -.regenerate-box, -.edit-box { - display: flex; - flex-direction: row; + .smartdraw-slider { + width: 65px; + } + + .regenerate-box, + .edit-box { + display: flex; + flex-direction: row; + } } diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index b4635673c..7db9ef133 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Slider, Switch } from '@mui/material'; -import { Button, IconButton } from 'browndash-components'; +import { Checkbox, FormControlLabel, Radio, RadioGroup, Slider, Switch } from '@mui/material'; +import { Button, IconButton } from '@dash/components'; import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; @@ -21,8 +21,12 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit'; import { InkingStroke } from '../InkingStroke'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { MarqueeView } from '../collections/collectionFreeForm'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView'; +import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; import './SmartDrawHandler.scss'; +import { Networking } from '../../Network'; +import { OpenWhere } from '../nodes/OpenWhere'; +import { FireflyDimensionsMap, FireflyImageDimensions, FireflyImageData } from './FireflyConstants'; +import { DocumentType } from '../../documents/DocumentTypes'; export interface DrawingOptions { text: string; @@ -55,22 +59,28 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 }; private _lastResponse: string = ''; - private _selectedDoc: Doc | undefined = undefined; + private _selectedDocs: Doc[] = []; private _errorOccurredOnce = false; @observable private _display: boolean = false; @observable private _pageX: number = 0; @observable private _pageY: number = 0; + @observable private _scale: number = 0; @observable private _yRelativeToTop: boolean = true; @observable private _isLoading: boolean = false; + @observable private _userInput: string = ''; + @observable private _regenInput: string = ''; @observable private _showOptions: boolean = false; @observable private _showEditBox: boolean = false; @observable private _complexity: number = 5; @observable private _size: number = 200; @observable private _autoColor: boolean = true; - @observable private _regenInput: string = ''; + @observable private _imgDims: FireflyImageDimensions = FireflyImageDimensions.Square; + @observable private _canInteract: boolean = true; + @observable private _generateDrawing: boolean = true; + @observable private _generateImage: boolean = true; @observable public ShowRegenerate: boolean = false; @@ -82,8 +92,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /** * AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e. - CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added - or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.) + CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added + or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.) */ public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction; public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction; @@ -105,14 +115,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { y: bounds.top - inkWidth / 2, _width: bounds.width + inkWidth, _height: bounds.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore inkWidth, opts.autoColor ? stroke[1] : ActiveInkColor(), ActiveInkBezierApprox(), - stroke[2] === 'none' ? ActiveFillColor() : stroke[2], - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), + stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2], + ActiveInkArrowStart(), + ActiveInkArrowEnd(), + ActiveInkDash(), ActiveIsInkMask() ); drawing.push(inkDoc); @@ -122,9 +132,10 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { }; @action - displaySmartDrawHandler = (x: number, y: number) => { + displaySmartDrawHandler = (x: number, y: number, scale: number) => { [this._pageX, this._pageY] = [x, y]; this._display = true; + this._scale = scale; }; /** @@ -134,14 +145,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { */ @action displayRegenerate = (x: number, y: number) => { - this._selectedDoc = DocumentView.SelectedDocs()?.lastElement(); + this._selectedDocs = [DocumentView.SelectedDocs()?.lastElement()]; [this._pageX, this._pageY] = [x, y]; this._display = false; this.ShowRegenerate = true; this._showEditBox = false; - const docData = this._selectedDoc[DocData]; + const docData = this._selectedDocs[0][DocData]; this._lastResponse = StrCast(docData.drawingData); - this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY }; + this._lastInput = { text: StrCast(docData.ai_drawing_input), complexity: NumCast(docData.ai_drawing_complexity), size: NumCast(docData.ai_drawing_size), autoColor: BoolCast(docData.ai_drawing_colored), x: this._pageX, y: this._pageY }; }; /** @@ -191,11 +202,13 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { */ @action handleSendClick = async () => { + if ((!this.ShowRegenerate && this._userInput == '') || (!this._generateImage && !this._generateDrawing)) return; this._isLoading = true; this._canInteract = false; if (this.ShowRegenerate) { - await this.regenerate(); + await this.regenerate(this._selectedDocs); runInAction(() => { + this._selectedDocs = []; this._regenInput = ''; this._showEditBox = false; }); @@ -204,7 +217,12 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { this._showOptions = false; }); try { - await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); + if (this._generateImage) { + await this.createImageWithFirefly(this._userInput); + } + if (this._generateDrawing) { + await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); + } this.hideSmartDrawHandler(); runInAction(() => { @@ -216,7 +234,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { this._errorOccurredOnce = false; } else { this._errorOccurredOnce = true; - await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); + await this.handleSendClick(); } } } @@ -229,59 +247,101 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /** * Calls GPT API to create a drawing based on user input. */ - @action drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { - if (input === '') return; - this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; - const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); - if (!res) { - console.error('GPT call failed'); - return; + if (input) { + this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; + const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); + if (res) { + const strokeData = await this.parseSvg(res, startPt, false, autoColor); + const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + drawingDoc && this._selectedDocs.push(drawingDoc); + this._errorOccurredOnce = false; + return strokeData; + } else { + console.error('GPT call failed'); + } } - const strokeData = await this.parseSvg(res, startPt, false, autoColor); - const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); - drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + return undefined; + }; - this._errorOccurredOnce = false; - return strokeData; + /** + * Calls Firefly API to create an image based on user input + */ + createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => { + this._lastInput.text = input; + const dims = FireflyDimensionsMap[this._imgDims]; + return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed }) + .then(img => { + const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1]; + if (!changeInPlace) { + const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { + title: input.match(/^(.*?)~~~.*$/)?.[1] || input, + nativeWidth: dims.width, + nativeHeight: dims.height, + ai: 'firefly', + ai_firefly_seed: newseed, + ai_firefly_prompt: input, + }); + DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight); + this._selectedDocs.push(imgDoc); + } + return { prompt: input, seed, pathname: img.accessPaths.agnostic.client }; + }) + .catch(e => alert('create image failed: ' + e.toString())); }; /** * Regenerates drawings with the option to add a specific regenerate prompt/request. + * @param doc the drawing Docs to regenerate */ @action - regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => { + regenerate = (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => { if (lastInput) this._lastInput = lastInput; if (lastResponse) this._lastResponse = lastResponse; if (regenInput) this._regenInput = regenInput; - - try { - let res; - if (this._regenInput !== '') { - const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`; - res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true); - this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`; - } else { - res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); - } - if (!res) { - console.error('GPT call failed'); - return; - } - const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); - this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc); - const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); - drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); - return strokeData; - } catch (err) { - console.error('Error regenerating drawing', err); - } + return Promise.all( + drawingDocs.map(async doc => { + switch (doc.type) { + case DocumentType.IMG: + if (this._regenInput) { + // if (this._selectedDoc) { + const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput; + return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace); + // } + } + return this.createImageWithFirefly(this._lastInput.text || StrCast(doc.ai_firefly_prompt), undefined, changeInPlace); + case DocumentType.COL: { + try { + let res; + if (this._regenInput) { + const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`; + res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true); + this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`; + } else { + res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); + } + if (res) { + const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); + this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc); + const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + } else { + console.error('GPT call failed'); + } + } catch (err) { + console.error('Error regenerating drawing', err); + } + break; + } + } + }) + ); }; /** * Parses the svg code that GPT returns into Bezier curves, with coordinates and colors. */ - @action parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => { const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g); if (svg) { @@ -292,7 +352,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { svgStrokes.forEach(child => { const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes); strokeData.push([ - convertedBezier.map(point => ({ X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 })), + convertedBezier.map(point => ({ X: startPoint.X + (point.X - startPoint.X) * this._scale, Y: startPoint.Y + (point.Y - startPoint.Y) * this._scale })), (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '', (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '', ]); @@ -342,10 +402,110 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { }); }, 'color strokes'); - renderDisplay() { + renderGenerateOutputOptions = () => ( + <div className="smartdraw-output-options"> + <div className="drawing-checkbox"> + Generate Ink + <Checkbox + sx={{ + color: 'white', + '&.Mui-checked': { + color: SettingsManager.userVariantColor, + }, + }} + checked={this._generateDrawing} + onChange={() => this._canInteract && (this._generateDrawing = !this._generateDrawing)} + /> + </div> + <div className="image-checkbox"> + Generate Image + <Checkbox + sx={{ + color: 'white', + '&.Mui-checked': { + color: SettingsManager.userVariantColor, + }, + }} + checked={this._generateImage} + onChange={() => this._canInteract && (this._generateImage = !this._generateImage)} + /> + </div> + </div> + ); + + renderGenerateDrawing = () => ( + <div className="smartdraw-options-container"> + Drawing Options + <div className="smartdraw-options"> + <div className="smartdraw-auto-color"> + Auto color + <Switch + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { color: SettingsManager.userColor }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: SettingsManager.userVariantColor }, + }} + defaultChecked={true} + value={this._autoColor} + size="small" + onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))} + /> + </div> + <div className="smartdraw-complexity"> + Complexity + <Slider + className="smartdraw-slider" + sx={{ + '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, + '& .MuiSlider-rail': { color: SettingsManager.userColor }, + '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, + }} + min={1} + max={10} + step={1} + size="small" + value={this._complexity} + onChange={action((e, val) => this._canInteract && (this._complexity = val as number))} + valueLabelDisplay="auto" + /> + </div> + <div className="smartdraw-size"> + Size (in pixels) + <Slider + className="smartdraw-slider" + sx={{ + '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, + '& .MuiSlider-rail': { color: SettingsManager.userColor }, + '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } }, + }} + min={50} + max={700} + step={10} + size="small" + value={this._size} + onChange={action((e, val) => this._canInteract && (this._size = val as number))} + valueLabelDisplay="auto" + /> + </div> + </div> + </div> + ); + + renderGenerateImage = () => ( + <div className="smartdraw-options-container"> + Image Options + <div className="smartdraw-dimensions"> + <RadioGroup row defaultValue="square" sx={{ alignItems: 'center' }}> + {Object.values(FireflyImageDimensions).map(dim => ( + <FormControlLabel sx={{ width: '40%' }} key={dim} value={dim} control={<Radio />} onChange={() => this._canInteract && (this._imgDims = dim)} label={dim} /> + ))} + </RadioGroup> + </div> + </div> + ); + + renderDisplay = () => { return ( <div - id="label-handler" className="smart-draw-handler" style={{ display: this._display ? '' : 'none', @@ -354,7 +514,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, }}> - <div> + <div className="smart-draw-main"> <IconButton tooltip="Cancel" onClick={() => { @@ -385,107 +545,65 @@ export class SmartDrawHandler extends ObservableReactComponent<object> { /> </div> {this._showOptions && ( - <div className="smartdraw-options"> - <div className="auto-color"> - Auto color - <Switch - sx={{ - '& .MuiSwitch-switchBase.Mui-checked': { color: SettingsManager.userColor }, - '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { backgroundColor: SettingsManager.userVariantColor }, - }} - defaultChecked={true} - value={this._autoColor} - size="small" - onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))} - /> - </div> - <div className="complexity"> - Complexity - <Slider - sx={{ - '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, - '& .MuiSlider-rail': { color: SettingsManager.userColor }, - '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, - }} - style={{ width: '80%' }} - min={1} - max={10} - step={1} - size="small" - value={this._complexity} - onChange={action((e, val) => this._canInteract && (this._complexity = val as number))} - valueLabelDisplay="auto" - /> - </div> - <div className="size"> - Size (in pixels) - <Slider - className="size-slider" - sx={{ - '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, - '& .MuiSlider-rail': { color: SettingsManager.userColor }, - '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } }, - }} - min={50} - max={700} - step={10} - size="small" - value={this._size} - onChange={action((e, val) => this._canInteract && (this._size = val as number))} - valueLabelDisplay="auto" - /> - </div> + <div> + {this.renderGenerateOutputOptions()} + {this._generateDrawing ? this.renderGenerateDrawing() : null} + {this._generateImage ? this.renderGenerateImage() : null} </div> )} </div> ); - } + }; - renderRegenerate() { - return ( - <div - className="smart-draw-handler" - style={{ - left: this._pageX, - ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), - background: SettingsManager.userBackgroundColor, - color: SettingsManager.userColor, - }}> - <div className="regenerate-box"> - <IconButton - tooltip="Regenerate" - icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />} - color={SettingsManager.userColor} - onClick={this.handleSendClick} - /> - <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> - {this._showEditBox && ( - <div className="edit-box"> - <input - aria-label="Edit instructions input" - className="smartdraw-input" - type="text" - value={this._regenInput} - onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} - onKeyDown={this.handleKeyPress} - placeholder="Edit instructions" - /> - <Button - style={{ alignSelf: 'flex-end' }} - text="Send" - icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} - iconPlacement="right" - color={SettingsManager.userColor} - onClick={this.handleSendClick} - /> - </div> - )} - </div> + renderRegenerateEditBox = () => ( + <div className="edit-box"> + <input + aria-label="Edit instructions input" + className="smartdraw-input" + type="text" + value={this._regenInput} + onChange={action(e => this._canInteract && (this._regenInput = e.target.value))} + onKeyDown={this.handleKeyPress} + placeholder="Edit instructions" + /> + <Button + style={{ alignSelf: 'flex-end' }} + text="Send" + icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + color={SettingsManager.userColor} + onClick={this.handleSendClick} + /> + </div> + ); + + renderRegenerate = () => ( + <div + className="smart-draw-handler" + style={{ + left: this._pageX, + ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, + }}> + <div className="regenerate-box"> + <IconButton + tooltip="Regenerate" + icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />} + color={SettingsManager.userColor} + onClick={this.handleSendClick} + /> + <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> + {this._showEditBox ? this.renderRegenerateEditBox() : null} </div> - ); - } + </div> + ); render() { - return this._display ? this.renderDisplay() : this.ShowRegenerate ? this.renderRegenerate() : null; + return this._display + ? this.renderDisplay() // + : this.ShowRegenerate + ? this.renderRegenerate() + : null; } } diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/StickerPalette.scss index 4f11e8afc..ca99410cf 100644 --- a/src/client/views/smartdraw/AnnotationPalette.scss +++ b/src/client/views/smartdraw/StickerPalette.scss @@ -1,4 +1,4 @@ -.annotation-palette { +.sticker-palette { display: flex; flex-direction: column; align-items: center; diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx new file mode 100644 index 000000000..d5307974f --- /dev/null +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -0,0 +1,352 @@ +import { Button } from '@dash/components'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Slider, Switch } from '@mui/material'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { AiOutlineSend } from 'react-icons/ai'; +import ReactLoading from 'react-loading'; +import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; +import { emptyFunction, numberRange } from '../../../Utils'; +import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { ImageCast, NumCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; +import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; +import { SettingsManager } from '../../util/SettingsManager'; +import { Transform } from '../../util/Transform'; +import { undoBatch } from '../../util/UndoManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; +import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { FieldView } from '../nodes/FieldView'; +import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; +import './StickerPalette.scss'; + +interface StickerPaletteProps { + Document: Doc; +} + +enum StickerPaletteMode { + create, + view, +} + +/** + * The StickerPalette can be toggled in the lightbox view of a document. The goal of the palette + * is to offer an easy way for users to create stickers and drag and drop them onto a document. + * These stickers can technically be of any document type and operate similarly to user templates. + * However, the palette is designed to be geared toward ink stickers and image stickers. + * + * On the "add" side of the palette, there is a way to create a drawing sticker with GPT. Users can + * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing + * to choose from. These drawings can then be saved to the palette as stickers. + */ +@observer +export class StickerPalette extends ObservableReactComponent<StickerPaletteProps> { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(StickerPalette, fieldKey); + } + /** + * Adds a doc to the sticker palette. Gets a snapshot of the document to use as a preview in the palette. When this + * preview is dragged onto a parent document, a copy of that document is added as a sticker. + */ + public static addToPalette = async (doc: Doc) => { + if (!doc.savedAsSticker) { + const docView = DocumentView.getDocumentView(doc); + await docView?.ComponentView?.updateIcon?.(true); + const { clone } = await Doc.MakeClone(doc); + clone.title = doc.title; + const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href; + Doc.AddDocToList(Doc.MyStickers, 'data', makeUserTemplateButtonOrImage(clone, image)); + doc.savedAsSticker = true; + } + }; + + public static getIcon(group: Doc) { + const docView = DocumentView.getDocumentView(group); + docView?.ComponentView?.updateIcon?.(true); + return !docView ? undefined : new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + + private _gptRes: string[] = []; + + @observable private _paletteMode = StickerPaletteMode.view; + @observable private _userInput: string = ''; + @observable private _isLoading: boolean = false; + @observable private _canInteract: boolean = true; + @observable private _showRegenerate: boolean = false; + @observable private _docView: DocumentView | null = null; + @observable private _docCarouselView: DocumentView | null = null; + @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + + constructor(props: StickerPaletteProps) { + super(props); + makeObservable(this); + } + + componentWillUnmount() { + this.resetPalette(true); + } + + Contains = (view: DocumentView) => + (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || // + (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); + + return170 = () => 170; + + handleKeyPress = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + this.generateDrawings(); + } + }; + + setPaletteMode = action((mode: StickerPaletteMode) => { + this._paletteMode = mode; + }); + + setUserInput = action((input: string) => { + if (!this._isLoading) this._userInput = input; + }); + + setDetail = action((detail: number) => { + if (this._canInteract) this._opts.complexity = detail; + }); + + setColor = action((autoColor: boolean) => { + if (this._canInteract) this._opts.autoColor = autoColor; + }); + + setSize = action((size: number) => { + if (this._canInteract) this._opts.size = size; + }); + + resetPalette = action((changePaletteMode: boolean) => { + if (changePaletteMode) this.setPaletteMode(StickerPaletteMode.view); + this.setUserInput(''); + this.setDetail(5); + this.setColor(true); + this.setSize(200); + this._showRegenerate = false; + this._canInteract = true; + this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + this._gptRes = []; + this._props.Document[DocData].data = undefined; + }); + + /** + * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from + * the sticker palette. + */ + @undoBatch + generateDrawings = action(() => { + this._isLoading = true; + const prevDrawings = DocListCast(this._props.Document[DocData].data); + this._props.Document[DocData].data = undefined; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + this._canInteract = false; + Promise.all( + numberRange(3).map(i => { + return this._showRegenerate + ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._opts, this._gptRes[i], this._userInput) + : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor); + }) + ).then(() => { + this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); + this._userInput = ''; + this._isLoading = false; + this._showRegenerate = true; + }); + }); + + @action + addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => { + this._gptRes.push(gptRes); + drawing[DocData].freeform_fitContentsToBox = true; + Doc.AddDocToList(this._props.Document, 'data', drawing); + }; + + /** + * Saves the currently showing, newly generated drawing to the sticker palette and sets the metadata. + * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user + * presses the "save drawing" button. + */ + saveDrawing = () => { + const cIndex = NumCast(this._props.Document.carousel_index); + const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; + const docData = focusedDrawing[DocData]; + docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text; + docData.ai_drawing_input = this._opts.text; + docData.ai_drawing_complexity = this._opts.complexity; + docData.ai_drawing_colored = this._opts.autoColor; + docData.ai_drawing_size = this._opts.size; + docData.ai_drawing_data = this._gptRes[cIndex]; + docData.ai = 'gpt'; + focusedDrawing.width = this._opts.size; + docData.x = this._opts.x; + docData.y = this._opts.y; + StickerPalette.addToPalette(focusedDrawing).then(() => this.resetPalette(true)); + }; + + renderCreateInput = () => ( + <div className="palette-create"> + <input + className="palette-create-input" + aria-label="label-input" + id="new-label" + type="text" + value={this._userInput} + onChange={e => this.setUserInput(e.target.value)} + placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'} + onKeyDown={this.handleKeyPress} + /> + <Button + style={{ alignSelf: 'flex-end' }} + tooltip={this._showRegenerate ? 'Regenerate' : 'Send'} + icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />} + iconPlacement="right" + color={SettingsManager.userColor} + onClick={this.generateDrawings} + /> + </div> + ); + renderCreateOptions = () => ( + <div className="palette-create-options"> + <div className="palette-color"> + Color + <Switch + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { + color: SettingsManager.userColor, + }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { + backgroundColor: SettingsManager.userVariantColor, + }, + }} + defaultChecked={true} + value={this._opts.autoColor} + size="small" + onChange={() => this.setColor(!this._opts.autoColor)} + /> + </div> + <div className="palette-detail"> + Detail + <Slider + sx={{ + '& .MuiSlider-thumb': { + color: SettingsManager.userColor, + '&.Mui-focusVisible, &:hover, &.Mui-active': { + boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`, + }, + }, + '& .MuiSlider-track': { + color: SettingsManager.userVariantColor, + }, + '& .MuiSlider-rail': { + color: SettingsManager.userColor, + }, + }} + style={{ width: '80%' }} + min={1} + max={10} + step={1} + size="small" + value={this._opts.complexity} + onChange={(e, val) => typeof val === 'number' && this.setDetail(val)} + valueLabelDisplay="auto" + /> + </div> + <div className="palette-size"> + Size + <Slider + sx={{ + '& .MuiSlider-thumb': { + color: SettingsManager.userColor, + '&.Mui-focusVisible, &:hover, &.Mui-active': { + boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`, + }, + }, + '& .MuiSlider-track': { + color: SettingsManager.userVariantColor, + }, + '& .MuiSlider-rail': { + color: SettingsManager.userColor, + }, + }} + style={{ width: '80%' }} + min={50} + max={500} + step={10} + size="small" + value={this._opts.size} + onChange={(e, val) => typeof val === 'number' && this.setSize(val)} + valueLabelDisplay="auto" + /> + </div> + </div> + ); + renderDoc = (doc: Doc, refFunc: (r: DocumentView) => void) => { + return ( + <DocumentView + ref={refFunc} + Document={doc} + addDocument={undefined} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={DocumentView.PinDoc} + containerViewPath={returnEmptyDocViewList} + styleProvider={DefaultStyleProvider} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return170} + PanelHeight={this.return170} + renderDepth={1} + isContentActive={returnTrue} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + ); + }; + renderPaletteCreate = () => ( + <> + {this.renderCreateInput()} + {this.renderCreateOptions()} + {this.renderDoc(this._props.Document, (r: DocumentView) => { + this._docCarouselView = r; + })} + <div className="palette-buttons"> + <Button text="Back" tooltip="Back to All Stickers" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} /> + <div className="palette-save-reset"> + <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} /> + <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} /> + </div> + </div> + </> + ); + renderPaletteView = () => ( + <> + {this.renderDoc(Doc.MyStickers, (r: DocumentView) => { + this._docView = r; + })} + <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode(StickerPaletteMode.create)} /> + </> + ); + + render() { + return ( + <div className="sticker-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}> + {this._paletteMode === StickerPaletteMode.view ? this.renderPaletteView() : null} + {this._paletteMode === StickerPaletteMode.create ? this.renderPaletteCreate() : null} + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { + layout: { view: StickerPalette, dataField: 'data' }, + options: { acl: '' }, +}); |
