From b5944e87f9d4f3149161de4de0d76db486461c76 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Wed, 30 Oct 2024 19:38:18 -0400 Subject: integrated image stickers with palette --- src/client/views/smartdraw/StickerPalette.tsx | 362 ++++++++++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/client/views/smartdraw/StickerPalette.tsx (limited to 'src/client/views/smartdraw/StickerPalette.tsx') diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx new file mode 100644 index 000000000..468d0fd13 --- /dev/null +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -0,0 +1,362 @@ +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 './StickerPalette.scss'; +import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; + +interface StickerPaletteProps { + Document: Doc; +} + +/** + * 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 { + @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: StickerPaletteProps) { + super(props); + makeObservable(this); + } + + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(StickerPalette, 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 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); + if (docView) { + docView.ComponentView?.updateIcon?.(true); + return new Promise(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 sticker 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 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 = 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]; + focusedDrawing.width = this._opts.size; + docData.x = this._opts.x; + docData.y = this._opts.y; + await StickerPalette.addToPalette(focusedDrawing); + this.resetPalette(true); + }; + + render() { + return ( +
e.stopPropagation()}> + {this._paletteMode === 'view' && ( + <> + (this._docView = r)} + Document={Doc.MyStickers} + 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} + /> +
+
+
+ Color + this.setColor(!this._opts.autoColor)} + /> +
+
+ Detail + { + this.setDetail(val as number); + }} + valueLabelDisplay="auto" + /> +
+
+ Size + { + this.setSize(val as number); + }} + valueLabelDisplay="auto" + /> +
+
+ (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} + /> +
+
+ + + )} + + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { + layout: { view: StickerPalette, dataField: 'data' }, + options: { acl: '' }, +}); -- cgit v1.2.3-70-g09d2 From e7162baa77a851d73037c63b75fac6a5c07f9b61 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Sat, 14 Dec 2024 14:13:59 -0500 Subject: replaced await in loop, added a componentWillUnmount to reset sticker palette --- src/client/views/nodes/imageEditor/ImageEditor.tsx | 10 ++- .../imageEditor/imageEditorUtils/BrushHandler.ts | 8 +- .../imageEditorUtils/imageEditorConstants.ts | 1 + src/client/views/smartdraw/DrawingFillHandler.tsx | 96 +++++++++++----------- src/client/views/smartdraw/StickerPalette.tsx | 26 +++--- 5 files changed, 70 insertions(+), 71 deletions(-) (limited to 'src/client/views/smartdraw/StickerPalette.tsx') diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index a742673e0..a39878924 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -28,9 +28,6 @@ import { PointerHandler } from './imageEditorUtils/PointerHandler'; import { activeColor, bgColor, brushWidthOffset, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './imageEditorUtils/imageEditorConstants'; import { CutMode, CursorData, ImageDimensions, ImageEditTool, ImageToolType, Point } from './imageEditorUtils/imageEditorInterfaces'; import { DocumentView } from '../DocumentView'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { ImageField } from '../../../../fields/URLField'; -import { resolve } from 'url'; import { DocData } from '../../../../fields/DocSymbols'; import { SettingsManager } from '../../../util/SettingsManager'; @@ -43,6 +40,13 @@ interface GenerativeFillProps { // Added field on image doc: gen_fill_children: List of children Docs +/** + * The image editor interface can be accessed by opening a document's context menu, then going to Options --> Open Image Editor. + * The image editor supports various operations on images. Currently, there is a Generative Fill feature that allows users to erase + * part of an image, add an optional prompt, and send this to GPT. GPT then returns a newly generated image that replaces the erased + * portion based on the optional prompt. There is also an image cutting tool that allows users to cut images in different ways to + * reshape the images, take out portions of images, and overall use them more creatively (see the header comment for cutImage() for more information). + */ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => { const canvasRef = useRef(null); const canvasBackgroundRef = useRef(null); diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts index a9fe02d4f..ed39375e0 100644 --- a/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/BrushHandler.ts @@ -1,12 +1,6 @@ import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers'; import { eraserColor } from './imageEditorConstants'; import { Point } from './imageEditorInterfaces'; -import { points } from '@turf/turf'; - -export enum BrushType { - GEN_FILL, - CUT, -} export class BrushHandler { static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => { @@ -20,7 +14,7 @@ export class BrushHandler { ctx.closePath(); }; - static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, brushType: BrushType) => { + static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string) => { const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint); const pts: Point[] = []; for (let i = 0; i < dist; i += 5) { diff --git a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts index 4772304bc..594d6d9fc 100644 --- a/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts +++ b/src/client/views/nodes/imageEditor/imageEditorUtils/imageEditorConstants.ts @@ -3,6 +3,7 @@ export const freeformRenderSize = 300; export const offsetDistanceY = freeformRenderSize + 400; export const offsetX = 200; export const newCollectionSize = 500; +export const brushWidthOffset = 10; export const activeColor = '#1976d2'; export const eraserColor = '#e1e9ec'; diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index c3c762181..8f71b150d 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -1,50 +1,50 @@ -import { action, makeObservable } from 'mobx'; -import { observer } from 'mobx-react'; -import React from 'react'; -import { Doc } from '../../../fields/Doc'; -import { ImageCast } from '../../../fields/Types'; -import { ImageField } from '../../../fields/URLField'; -import { Docs } from '../../documents/Documents'; -import { Networking } from '../../Network'; -import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; -import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; -import { ImageUtility } from '../nodes/imageEditor/imageEditorUtils/ImageHandler'; -import { OpenWhere } from '../nodes/OpenWhere'; -import { ObservableReactComponent } from '../ObservableReactComponent'; +// import { action, makeObservable } from 'mobx'; +// import { observer } from 'mobx-react'; +// import React from 'react'; +// import { Doc } from '../../../fields/Doc'; +// import { ImageCast } from '../../../fields/Types'; +// import { ImageField } from '../../../fields/URLField'; +// import { Docs } from '../../documents/Documents'; +// import { Networking } from '../../Network'; +// import { makeUserTemplateButtonOrImage } from '../../util/DropConverter'; +// import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +// import { ImageUtility } from '../nodes/imageEditor/imageEditorUtils/ImageHandler'; +// import { OpenWhere } from '../nodes/OpenWhere'; +// import { ObservableReactComponent } from '../ObservableReactComponent'; -export class DrawingFillHandler { - static drawingToImage = async (drawing: Doc, prompt: string) => { - const imageField = await DocumentView.GetDocImage(drawing); - if (!imageField) return; - const { href } = ImageCast(imageField).url; - const hrefParts = href.split('.'); - const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; - try { - const response = await fetch(hrefComplete); - const blob: Blob = await response.blob(); - const strength: number = 100; - const img = await Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }); - DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight); - // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight)); - } catch (error) { - console.error('Error fetching image:', error); - return; - } +// export class DrawingFillHandler { +// static drawingToImage = async (drawing: Doc, prompt: string) => { +// const imageField = await DocumentView.GetDocImage(drawing); +// if (!imageField) return; +// const { href } = ImageCast(imageField).url; +// const hrefParts = href.split('.'); +// const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; +// try { +// const response = await fetch(hrefComplete); +// const blob: Blob = await response.blob(); +// const strength: number = 100; +// const img = await Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }); +// DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight); +// // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight)); +// } catch (error) { +// console.error('Error fetching image:', error); +// return; +// } - // const image = new Image(); - // image.src = imageField.url?.href; - // // image.onload = async () => { - // const canvas = document.createElement('canvas'); - // canvas.width = image.width; - // canvas.height = image.height; - // const ctx = canvas.getContext('2d'); - // if (!ctx) return; - // ctx.globalCompositeOperation = 'source-over'; - // ctx.clearRect(0, 0, image.width, image.height); - // ctx.drawImage(image, 0, 0); - // const blob: Blob = await ImageUtility.canvasToBlob(canvas); - // const strength: number = 100; - // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight)); - // }; - }; -} +// // const image = new Image(); +// // image.src = imageField.url?.href; +// // // image.onload = async () => { +// // const canvas = document.createElement('canvas'); +// // canvas.width = image.width; +// // canvas.height = image.height; +// // const ctx = canvas.getContext('2d'); +// // if (!ctx) return; +// // ctx.globalCompositeOperation = 'source-over'; +// // ctx.clearRect(0, 0, image.width, image.height); +// // ctx.drawImage(image, 0, 0); +// // const blob: Blob = await ImageUtility.canvasToBlob(canvas); +// // const strength: number = 100; +// // Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight)); +// // }; +// }; +// } diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index 468d0fd13..d56878f10 100644 --- a/src/client/views/smartdraw/StickerPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -56,6 +56,10 @@ export class StickerPalette extends ObservableReactComponent { 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); - } - } + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + this._canInteract = false; + await Promise.all( + Array.from({ length: 3 }).map((_, i) => { + return this._showRegenerate + ? SmartDrawHandler.Instance.regenerate(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); + }) + ); this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); this._userInput = ''; this._isLoading = false; -- cgit v1.2.3-70-g09d2 From 0eff48b757ca81860a883d25e147b8a869e5fe00 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Mon, 30 Dec 2024 23:35:24 -0500 Subject: created image regeneration with dialogue --- src/client/apis/gpt/GPT.ts | 39 +++- src/client/documents/Documents.ts | 4 + src/client/views/PropertiesView.scss | 1 + src/client/views/PropertiesView.tsx | 12 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 22 +-- src/client/views/nodes/ImageBox.tsx | 10 ++ src/client/views/nodes/imageEditor/ImageEditor.tsx | 2 +- src/client/views/pdf/AnchorMenu.tsx | 1 + src/client/views/smartdraw/DrawingFillHandler.tsx | 11 +- src/client/views/smartdraw/SmartDrawHandler.scss | 8 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 197 ++++++++++++++------- src/client/views/smartdraw/StickerPalette.tsx | 1 + src/server/ApiManagers/FireflyManager.ts | 20 ++- src/server/DashUploadUtils.ts | 1 - 14 files changed, 238 insertions(+), 91 deletions(-) (limited to 'src/client/views/smartdraw/StickerPalette.tsx') diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 03380e4d6..9241eb120 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -249,6 +249,41 @@ const gptHandwriting = async (src: string): Promise => { } }; +const gptDescribeImage = async (image: string): Promise => { + try { + const response = await openai.chat.completions.create({ + model: 'gpt-4o', + temperature: 0, + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: `Identify what this drawing is, naming as many elements and their location in the drawing as possible`, + }, + { + type: 'image_url', + image_url: { + url: `${image}`, + detail: 'low', + }, + }, + ], + }, + ], + }); + if (response.choices[0].message.content) { + console.log('GPT DESCRIPTION', response.choices[0].message.content); + return response.choices[0].message.content; + } + return 'Unknown drawing'; + } catch (err) { + console.log(err); + return 'Error connecting with API'; + } +}; + const gptDrawingColor = async (image: string, coords: string[]): Promise => { try { const response = await openai.chat.completions.create({ @@ -276,11 +311,11 @@ const gptDrawingColor = async (image: string, coords: string[]): Promise if (response.choices[0].message.content) { return response.choices[0].message.content; } - return 'Missing labels'; + return 'Unknown drawing'; } catch (err) { console.log(err); return 'Error connecting with API'; } }; -export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDrawingColor }; +export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDescribeImage, gptDrawingColor }; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index c51c1645d..785af3409 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -516,6 +516,10 @@ export class DocumentOptions { card_sort?: STRt = new StrInfo('way cards are sorted in deck view'); card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending'); + + ai_generated?: boolean; // to mark items as ai generated + firefly_seed?: number; + firefly_prompt?: string; } export const DocOptions = new DocumentOptions(); diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 693c75ebf..7866e67e7 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -642,6 +642,7 @@ .smooth, .color, +.strength-slider, .smooth-slider { margin-top: 7px; } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index aefdeee17..5b24eb7ea 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -982,6 +982,9 @@ export class PropertiesView extends ObservableReactComponent { + !isNaN(val) && (this.refStrength = val); + }); return (
{!targetDoc.layout_isSvg && this.containsInkDoc && ( @@ -995,9 +998,10 @@ export class PropertiesView extends ObservableReactComponent DrawingFillHandler.drawingToImage(targetDoc, 'fill in the details of this image'), 'createImage')} + onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, 'fill in the details of this image'), 'createImage')} />
+
{strength}
{ + doc[DocData].drawing_refStrength = Number(value); + }); + } @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore set smoothAmt(value) { this.selectedStrokes.forEach(doc => { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index acf72e5cb..4bccdd286 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1295,6 +1295,7 @@ export class CollectionFreeFormView extends CollectionSubView { - SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; - !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); - }), - icon: 'pen-to-square', - }); + this.layoutDoc.drawingData != undefined && + optionItems.push({ + description: 'Show Drawing Editor', + event: action(() => { + SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); + }), + icon: 'pen-to-square', + }); optionItems.push({ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 25e7b566f..8f6a90e61 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -40,6 +40,7 @@ import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; import { Upload } from '../../../server/SharedMediaTypes'; +import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -351,6 +352,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }), icon: 'pencil-alt', }); + this.layoutDoc.ai_generated && + funcs.push({ + description: 'Regenerate AI Image', + event: action(() => { + console.log('COOOORDS', this.dataDoc.width as number, this.dataDoc.y as number); + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this.dataDoc.x as number, (this.dataDoc.y as number) - 10) : SmartDrawHandler.Instance.hideRegenerate(); + }), + icon: 'pen-to-square', + }); funcs.push({ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx index a39878924..2a8bc034d 100644 --- a/src/client/views/nodes/imageEditor/ImageEditor.tsx +++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx @@ -411,7 +411,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc let finalImgURL: string = url; // crop the image for these brush modes to remove excess blank space around the image contents if (currCutType == CutMode.IN || currCutType == CutMode.DRAW_IN) { - const croppedData = cropImage(image, minX, maxX, minY, maxY); + const croppedData = cropImage(image, Math.max(minX, 0), Math.min(maxX, image.width), Math.max(minY, 0), Math.min(maxY, image.height)); finalImg = croppedData; finalImgURL = croppedData.src; } diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index fe03f32a5..bb8082061 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -158,6 +158,7 @@ export class AnchorMenu extends AntimodeMenu { docData.drawingColored = opts.autoColor; docData.drawingSize = opts.size; docData.drawingData = gptRes; + docData.ai_generated = true; }); pointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index 48e71bc9f..1a470f995 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -1,21 +1,26 @@ +import { imageUrlToBase64 } from '../../../ClientUtils'; import { Doc } from '../../../fields/Doc'; import { ImageCast } from '../../../fields/Types'; 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'; export class DrawingFillHandler { - static drawingToImage = (drawing: Doc, prompt: string) => + static drawingToImage = (drawing: Doc, strength: number, prompt: string) => DocumentView.GetDocImage(drawing)?.then(imageField => { if (imageField) { const { href } = ImageCast(imageField).url; const hrefParts = href.split('.'); const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`; - const strength: number = 100; - Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => + imageUrlToBase64(structureUrl) + .then((hrefBase64: string) => gptDescribeImage(hrefBase64)) + .then((prompt: string) => { + Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {}), OpenWhere.addRight)) // prettier-ignore + }); } return false; }); diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss index c25273876..513779512 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.scss +++ b/src/client/views/smartdraw/SmartDrawHandler.scss @@ -12,7 +12,13 @@ } } - .smartdraw-options { + .smartdraw-output-options { + display: flex; + flex-direction: row; + justify-content: center; + } + + .smartdraw-svg-options { margin-top: 5px; display: flex; flex-direction: row; diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 036ac5983..fb1a5771e 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Slider, Switch } from '@mui/material'; +import { Checkbox, Slider, Switch } from '@mui/material'; import { Button, IconButton } from 'browndash-components'; import { action, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -74,6 +74,8 @@ export class SmartDrawHandler extends ObservableReactComponent { @observable private _autoColor: boolean = true; @observable private _regenInput: string = ''; @observable private _canInteract: boolean = true; + @observable private _generateDrawing: boolean = true; + @observable private _generateImage: boolean = true; @observable public ShowRegenerate: boolean = false; @@ -195,6 +197,7 @@ export class SmartDrawHandler extends ObservableReactComponent { */ @action handleSendClick = async () => { + if (!this._generateImage && !this._generateDrawing) return; this._isLoading = true; this._canInteract = false; if (this.ShowRegenerate) { @@ -212,7 +215,12 @@ export class SmartDrawHandler extends ObservableReactComponent { 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(() => { @@ -240,15 +248,12 @@ export class SmartDrawHandler extends ObservableReactComponent { drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { if (input) { this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; - - Networking.PostToServer('/queryFireflyImage', { prompt: input }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { title: input }), OpenWhere.addRight)); - 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); - + this._selectedDoc = drawingDoc; this._errorOccurredOnce = false; return strokeData; } else { @@ -258,6 +263,23 @@ export class SmartDrawHandler extends ObservableReactComponent { return undefined; }; + /** + * Calls Firefly API to create an image based on user input + */ + createImageWithFirefly = (input: string, seed?: number) => { + this._lastInput.text = input; + return Networking.PostToServer('/queryFireflyImage', { prompt: input, seed: seed }).then(img => { + const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, { + title: input.match(/^(.*?)~~~.*$/)?.[1] || input, + ai_generated: true, + firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1], + firefly_prompt: input, + }); + DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight); + this._selectedDoc = imgDoc; + }); + }; + /** * Regenerates drawings with the option to add a specific regenerate prompt/request. */ @@ -266,27 +288,39 @@ export class SmartDrawHandler extends ObservableReactComponent { if (lastInput) this._lastInput = lastInput; if (lastResponse) this._lastResponse = lastResponse; if (regenInput) this._regenInput = regenInput; - - try { - let res; + if (this._generateDrawing) { + 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); + } catch (err) { + console.error('Error regenerating drawing', err); + } + } + if (this._generateImage) { 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}`; + if (this._selectedDoc) { + const docData = this._selectedDoc[DocData]; + const newPrompt = `${docData.firefly_prompt}, ${this._regenInput}`; + const seed: number = docData?.firefly_seed as number; + await this.createImageWithFirefly(newPrompt, seed); + } } else { - res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); + await this.createImageWithFirefly(this._lastInput.text); } - 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); } }; @@ -397,58 +431,87 @@ export class SmartDrawHandler extends ObservableReactComponent { {this._showOptions && (
-
-
- Auto color - this._canInteract && (this._autoColor = !this._autoColor))} - /> -
-
- Complexity - +
+ Generate Ink + this._canInteract && (this._complexity = val as number))} - valueLabelDisplay="auto" + checked={this._generateDrawing} + onChange={() => this._canInteract && (this._generateDrawing = !this._generateDrawing)} />
-
- Size (in pixels) - + Generate Image + this._canInteract && (this._size = val as number))} - valueLabelDisplay="auto" + checked={this._generateImage} + onChange={() => this._canInteract && (this._generateImage = !this._generateImage)} />
-
+ {this._generateDrawing && ( +
+
+ Auto color + this._canInteract && (this._autoColor = !this._autoColor))} + /> +
+
+ Complexity + this._canInteract && (this._complexity = val as number))} + valueLabelDisplay="auto" + /> +
+
+ Size (in pixels) + this._canInteract && (this._size = val as number))} + valueLabelDisplay="auto" + /> +
+
+ )}
)}
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index d56878f10..352a02e32 100644 --- a/src/client/views/smartdraw/StickerPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -186,6 +186,7 @@ export class StickerPalette extends ObservableReactComponent { + generateImage = (prompt: string = 'a realistic illustration of a cat coding', seed?: number) => { + let body = `{ "prompt": "${prompt}" }`; + if (seed) { + body = `{ "prompt": "${prompt}", "seeds": [${seed}]}`; + } const fetched = this.getBearerToken().then(response => response?.json().then((data: { access_token: string }) => fetch('https://firefly-api.adobe.io/v3/images/generate', { @@ -76,9 +80,15 @@ export default class FireflyManager extends ApiManager { ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''], ['Authorization', `Bearer ${data.access_token}`], ], - body: `{ "prompt": "${prompt}" }`, + body: body, }) - .then(response2 => response2.json().then(json => (json.outputs?.[0] as { image: { url: string } })?.image.url)) + .then(response2 => + response2.json().then(json => { + const seed = json.outputs?.[0]?.seed; + const url = json.outputs?.[0]?.image?.url; + return { seed, url }; + }) + ) .catch(error => { console.error('Error:', error); return undefined; @@ -226,8 +236,8 @@ export default class FireflyManager extends ApiManager { method: Method.POST, subscription: '/queryFireflyImage', secureHandler: ({ req, res }) => - this.generateImage(req.body.prompt).then(url => - DashUploadUtils.UploadImage(url ?? '').then(info => { + this.generateImage(req.body.prompt, req.body.seed).then(img => + DashUploadUtils.UploadImage(img?.url ?? '', undefined, img?.seed).then(info => { if (info instanceof Error) _invalid(res, info.message); else _success(res, info); }) diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 623172894..2177c5d97 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -458,7 +458,6 @@ export namespace DashUploadUtils { return { name: result.name, message: result.message }; } const outputFile = filename || result.filename || ''; - return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false); }; -- cgit v1.2.3-70-g09d2 From 1d62d867621b293c41ff8488ca5a3bd6010723d5 Mon Sep 17 00:00:00 2001 From: eleanor-park Date: Sun, 5 Jan 2025 23:47:18 -0500 Subject: added AI image editor --- src/client/documents/Documents.ts | 6 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/DocumentButtonBar.tsx | 7 +- src/client/views/ViewBoxInterface.ts | 1 + .../collectionFreeForm/CollectionFreeFormView.tsx | 12 +-- src/client/views/nodes/DocumentView.tsx | 22 +++- src/client/views/nodes/ImageBox.scss | 28 +++++ src/client/views/nodes/ImageBox.tsx | 113 ++++++++++++++++++++- src/client/views/pdf/AnchorMenu.tsx | 12 +-- src/client/views/smartdraw/DrawingFillHandler.tsx | 2 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 92 +++++++++-------- src/client/views/smartdraw/StickerPalette.tsx | 17 ++-- src/server/ApiManagers/FireflyManager.ts | 1 - 13 files changed, 235 insertions(+), 80 deletions(-) (limited to 'src/client/views/smartdraw/StickerPalette.tsx') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 785af3409..7f1387ff8 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -517,9 +517,9 @@ export class DocumentOptions { card_sort?: STRt = new StrInfo('way cards are sorted in deck view'); card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending'); - ai_generated?: boolean; // to mark items as ai generated - firefly_seed?: number; - firefly_prompt?: string; + ai?: string; // to mark items as ai generated + ai_firefly_seed?: number; + ai_firefly_prompt?: string; } export const DocOptions = new DocumentOptions(); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 9af79a02e..b41fd09dc 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -779,7 +779,7 @@ pie title Minerals in my tap water { title: " Size", toolTip: "Size of area pencil eraser", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.EraserWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40}, { title: "Mask", toolTip: "Make Stroke a Stencil Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", toolType: InkProperty.Mask, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, { title: "Labels", toolTip: "Show Labels Inside Shapes", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: InkProperty.Labels, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}}, - { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: InkTool.SmartDraw, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}}, + { title: "Smart Draw", toolTip: "Draw with AI", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: InkTool.SmartDraw, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}}, ]; } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index b7033af3f..d722b28b5 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -319,12 +319,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( const targetDoc = this.view0?.Document; return !targetDoc ? null : ( Edit with AI}> -
{ - CalendarManager.Instance.open(this.view0, targetDoc); - }}> +
this.view0?.toggleAIEditor(), 'toggle AI editor')}>
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts index b7980d74e..df08f2564 100644 --- a/src/client/views/ViewBoxInterface.ts +++ b/src/client/views/ViewBoxInterface.ts @@ -60,4 +60,5 @@ export abstract class ViewBoxInterface

extends ObservableReactComponent boolean; dontRegisterView?: () => boolean; // KeyValueBox's don't want to register their views isUnstyledView?: () => boolean; // SchemaView and KeyValue are unstyled -- not titles, no opacity, no animations + componentAIView?: (top: number) => JSX.Element; } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ef0b80720..9af698ec7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1290,12 +1290,12 @@ export class CollectionFreeFormView extends CollectionSubView> = undefined; // needs to be accessed from DocumentView wrapper class @observable _animateScaleTime: Opt = undefined; // milliseconds for animating between views. defaults to 300 if not uset @observable _animateScalingTo = 0; + @observable public _showAIEditor: boolean = false; + + @action + showAIEditor() { + this._showAIEditor = !this._showAIEditor; + } get _contentDiv() { return this._mainCont.current; } // prettier-ignore get _docView() { return this._props.DocumentView?.(); } // prettier-ignore @@ -552,7 +558,6 @@ export class DocumentViewInternal extends DocComponent DocumentView.SetLightboxDoc(this.Document), icon: 'external-link-alt' }); } appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'map-pin' }); - appearanceItems.push({ description: 'Make Image', event: () => DrawingFillHandler.drawingToImage(this.Document, StrCast(this.Document.title)), icon: 'map-pin' }); !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' }); @@ -711,6 +716,8 @@ export class DocumentViewInternal extends DocComponent this._props.PanelWidth() * 0.6; + rph = () => this.panelHeight() * 0.6; @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; @@ -720,7 +727,9 @@ export class DocumentViewInternal extends DocComponent + {this._showAIEditor && (this._componentView?.componentAIView?.(this.rph()) ?? null)}

); } @@ -1286,6 +1297,11 @@ export class DocumentView extends DocComponent() { } }; + @action + public toggleAIEditor = () => { + this._docViewInternal && this._docViewInternal.showAIEditor(); + }; + public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => { this._htmlOverlayText = text; this._htmlOverlayEffect = effect; diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss index 3ffda5a35..03314e90f 100644 --- a/src/client/views/nodes/ImageBox.scss +++ b/src/client/views/nodes/ImageBox.scss @@ -139,3 +139,31 @@ .imageBox-fadeBlocker-hover { opacity: 0; } + +.imageBox-aiView { + padding: 5px; + position: absolute; + overflow: scroll; + text-align: center; + font-weight: bold; + margin-top: 5px; + + .imageBox-aiView-subtitle { + align-self: start; + } + + .imageBox-aiView-regenerate-container, + .imageBox-aiView-options-container { + font-weight: normal; + text-align: start; + } + + .imageBox-aiView-regenerate, + .imageBox-aiView-options { + display: flex; + flex-direction: row; + justify-content: center; + flex-direction: row; + gap: 5px; + } +} diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 7ce429f0f..f00580d77 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -1,7 +1,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import axios from 'axios'; -import { Colors } from 'browndash-components'; +import { Colors, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; @@ -41,6 +41,9 @@ import './ImageBox.scss'; import { OpenWhere } from './OpenWhere'; import { Upload } from '../../../server/SharedMediaTypes'; import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; +import { Button } from 'browndash-components'; +import { SettingsManager } from '../../util/SettingsManager'; +import { AiOutlineSend } from 'react-icons/ai'; export class ImageEditorData { // eslint-disable-next-line no-use-before-define @@ -352,7 +355,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { }), icon: 'pencil-alt', }); - this.layoutDoc.ai_generated && + this.layoutDoc.ai && funcs.push({ description: 'Regenerate AI Image', event: action(e => { @@ -526,6 +529,112 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { ); } + @observable private _regenInput = ''; + @observable private _canInteract = true; + @observable private _regenerateLoading = false; + + componentAIView = (top: number) => { + const field = Cast(this.dataDoc[this.fieldKey], ImageField); + const showRegenerate = this.Document[DocData].ai; + return ( +
+ Edit Image with AI + {showRegenerate && ( +
+ Regenerate AI Image +
+ this._canInteract && (this._regenInput = e.target.value))} + // onKeyDown={this.handleKeyPress} + placeholder="Prompt (Optional)" + /> +
+
+ )} +
+ {showRegenerate && More Image Options } +
+
+
+
+ ); + }; + @computed get annotationLayer() { TraceMobx(); return
; diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index bb8082061..2e704aa8d 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -153,12 +153,12 @@ export class AnchorMenu extends AntimodeMenu { this.AddDrawingAnnotation(drawing); const docData = drawing[DocData]; docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; - docData.drawingInput = opts.text; - docData.drawingComplexity = opts.complexity; - docData.drawingColored = opts.autoColor; - docData.drawingSize = opts.size; - docData.drawingData = gptRes; - docData.ai_generated = true; + docData.ai_drawing_input = opts.text; + docData.ai_drawing_complexity = opts.complexity; + docData.ai_drawing_colored = opts.autoColor; + docData.ai_drawing_size = opts.size; + docData.ai_drawing_data = gptRes; + docData.ai = 'gpt'; }); pointerDown = (e: React.PointerEvent) => { diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index 52652d377..8e41ee105 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -35,7 +35,7 @@ export class DrawingFillHandler { .then((hrefBase64: string) => gptDescribeImage(hrefBase64)) .then((prompt: string) => { Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl: structureUrl, strength: strength, styles: styles }).then((info: Upload.ImageInformation) => - DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight) + DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai: 'firefly', ai_firefly_prompt: prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight) ); // prettier-ignore }); } diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 6c9470480..0c67c7a13 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -58,7 +58,7 @@ export class SmartDrawHandler extends ObservableReactComponent { 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; @@ -144,14 +144,14 @@ export class SmartDrawHandler extends ObservableReactComponent { */ @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 }; }; /** @@ -205,8 +205,9 @@ export class SmartDrawHandler extends ObservableReactComponent { 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; }); @@ -253,7 +254,7 @@ export class SmartDrawHandler extends ObservableReactComponent { 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); - this._selectedDoc = drawingDoc; + drawingDoc && this._selectedDocs.push(drawingDoc); this._errorOccurredOnce = false; return strokeData; } else { @@ -274,57 +275,62 @@ export class SmartDrawHandler extends ObservableReactComponent { title: input.match(/^(.*?)~~~.*$/)?.[1] || input, nativeWidth: dims.width, nativeHeight: dims.height, - ai_generated: true, - firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1], - firefly_prompt: input, + ai: 'firefly', + ai_firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1], + ai_firefly_prompt: input, }); DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight); - this._selectedDoc = imgDoc; + this._selectedDocs.push(imgDoc); }); }; /** * 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 = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => { if (lastInput) this._lastInput = lastInput; if (lastResponse) this._lastResponse = lastResponse; if (regenInput) this._regenInput = regenInput; - if (this._generateImage) { - if (this._regenInput !== '') { - if (this._selectedDoc) { - const docData = this._selectedDoc[DocData]; - const newPrompt = `${docData.firefly_prompt}, ${this._regenInput}`; - const seed: number = docData?.firefly_seed as number; - await this.createImageWithFirefly(newPrompt, seed); + await Promise.all( + drawingDocs.map(async doc => { + const docData = doc[DocData]; + if (docData.type == 'image') { + const seed: number = docData?.ai_firefly_seed as number; + if (this._regenInput !== '') { + // if (this._selectedDoc) { + const newPrompt = `${docData.ai_firefly_prompt}, ${this._regenInput}`; + await this.createImageWithFirefly(newPrompt, seed); + // } + } else { + await this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt)); + } } - } else { - await this.createImageWithFirefly(this._lastInput.text); - } - } - if (this._generateDrawing) { - 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; + if (docData.type == 'collection') { + 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, doc); + const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + } catch (err) { + console.error('Error regenerating drawing', err); + } } - 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); - } catch (err) { - console.error('Error regenerating drawing', err); - } - } + }) + ); }; /** diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index 352a02e32..d23763eb9 100644 --- a/src/client/views/smartdraw/StickerPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -142,19 +142,20 @@ export class StickerPalette extends ObservableReactComponent { 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; await Promise.all( Array.from({ length: 3 }).map((_, i) => { return this._showRegenerate - ? SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput) + ? 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); }) ); @@ -181,12 +182,12 @@ export class StickerPalette extends ObservableReactComponent { - console.log('DIMENSIONS', width, height); let body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`; if (seed) { console.log('RECEIVED SEED', seed); -- cgit v1.2.3-70-g09d2 From 8da4acee81701b2b779713b1ff4cc4d60e80f437 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 8 Jan 2025 10:29:50 -0500 Subject: code cleanup --- src/client/views/nodes/ImageBox.tsx | 10 +- src/client/views/smartdraw/SmartDrawHandler.tsx | 366 ++++++++++---------- src/client/views/smartdraw/StickerPalette.tsx | 434 ++++++++++++------------ 3 files changed, 404 insertions(+), 406 deletions(-) (limited to 'src/client/views/smartdraw/StickerPalette.tsx') diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index a1fa9a283..ba0959d73 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -7,8 +7,8 @@ import { observer } from 'mobx-react'; import { extname } from 'path'; import * as React from 'react'; import ReactLoading from 'react-loading'; -import { ClientUtils, DashColor, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; -import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc'; +import { ClientUtils, DashColor, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../ClientUtils'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; @@ -34,7 +34,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu'; import { PinDocView, PinProps } from '../PinFuncs'; import { StickerPalette } from '../smartdraw/StickerPalette'; import { StyleProp } from '../StyleProp'; -import { DocumentView, DocumentViewInternal } from './DocumentView'; +import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import './ImageBox.scss'; @@ -44,7 +44,6 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; import { Button } from 'browndash-components'; import { SettingsManager } from '../../util/SettingsManager'; import { AiOutlineSend } from 'react-icons/ai'; -import { returnEmptyDocViewList } from '../StyleProvider'; import { FireflyImageData } from '../smartdraw/FireflyConstants'; export class ImageEditorData { @@ -541,6 +540,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() {
{this._prevImgs.map(img => ( { @@ -583,7 +583,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { onClick={undoable( action(async () => { this._regenerateLoading = true; - SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => { + await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => { if (newImgs[0]) { const url = newImgs[0].pathname; const imgField = new ImageField(url); diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index 5ebe2e358..58db091bc 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -26,6 +26,7 @@ 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; @@ -272,19 +273,19 @@ export class SmartDrawHandler extends ObservableReactComponent { const dims = FireflyDimensionsMap[this._imgDims]; return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => { if (!changeInPlace) { - const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1]; + const aiseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1]; 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: seed, + ai_firefly_seed: aiseed, ai_firefly_prompt: input, }); DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight); this._selectedDocs.push(imgDoc); } - return { prompt: input, seed: seed, pathname: img.accessPaths.agnostic.client }; + return { prompt: input, seed, pathname: img.accessPaths.agnostic.client }; }); }; @@ -293,44 +294,43 @@ export class SmartDrawHandler extends ObservableReactComponent { * @param doc the drawing Docs to regenerate */ @action - regenerate = async (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => { + 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; - return await Promise.all( + return Promise.all( drawingDocs.map(async doc => { - const docData = doc[DocData]; - if (docData.type == 'image') { - const seed: number = docData?.ai_firefly_seed as number; - if (this._regenInput !== '') { - // if (this._selectedDoc) { - const newPrompt = `${docData.ai_firefly_prompt} ~~~ ${this._regenInput}`; - return this.createImageWithFirefly(newPrompt, seed, changeInPlace); - // } - } else { - return this.createImageWithFirefly(this._lastInput.text || StrCast(docData.ai_firefly_prompt), undefined, changeInPlace); - } - } - if (docData.type == 'collection') { - 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); + switch (doc.type) { + case DocumentType.IMG: + if (this._regenInput) { + // if (this._selectedDoc) { + const newPrompt = `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}`; + return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace); + // } } - if (!res) { - console.error('GPT call failed'); - return; + 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); } - 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); - } catch (err) { - console.error('Error regenerating drawing', err); + break; } } }) @@ -340,7 +340,6 @@ export class SmartDrawHandler extends ObservableReactComponent { /** * 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(/]*>([\s\S]*?)<\/svg>/g); if (svg) { @@ -401,7 +400,108 @@ export class SmartDrawHandler extends ObservableReactComponent { }); }, 'color strokes'); - renderDisplay() { + renderGenerateOutputOptions = () => ( +
+
+ Generate Ink + this._canInteract && (this._generateDrawing = !this._generateDrawing)} + /> +
+
+ Generate Image + this._canInteract && (this._generateImage = !this._generateImage)} + /> +
+
+ ); + + renderGenerateDrawing = () => ( +
+ Drawing Options +
+
+ Auto color + this._canInteract && (this._autoColor = !this._autoColor))} + /> +
+
+ Complexity + this._canInteract && (this._complexity = val as number))} + valueLabelDisplay="auto" + /> +
+
+ Size (in pixels) + this._canInteract && (this._size = val as number))} + valueLabelDisplay="auto" + /> +
+
+
+ ); + + renderGenerateImage = () => ( +
+ Image Options +
+ + {Object.values(FireflyImageDimensions).map(dim => ( + } onChange={() => this._canInteract && (this._imgDims = dim)} label={dim} /> + ))} + +
+
+ ); + + renderDisplay = () => { return (
{
{this._showOptions && (
-
-
- Generate Ink - this._canInteract && (this._generateDrawing = !this._generateDrawing)} - /> -
-
- Generate Image - this._canInteract && (this._generateImage = !this._generateImage)} - /> -
-
- {this._generateDrawing && ( -
- Drawing Options -
-
- Auto color - this._canInteract && (this._autoColor = !this._autoColor))} - /> -
-
- Complexity - this._canInteract && (this._complexity = val as number))} - valueLabelDisplay="auto" - /> -
-
- Size (in pixels) - this._canInteract && (this._size = val as number))} - valueLabelDisplay="auto" - /> -
-
-
- )} - {this._generateImage && ( -
- Image Options -
- - } onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Square)} label="Square" /> - } onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Landscape)} label="Landscape" /> - } onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Portrait)} label="Portrait" /> - } onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Widescreen)} label="Widescreen" /> - -
-
- )} + {this.renderGenerateOutputOptions()} + {this._generateDrawing ? this.renderGenerateDrawing() : null} + {this._generateImage ? this.renderGenerateImage() : null}
)} ); - } + }; - renderRegenerate() { - return ( -
-
- : } - color={SettingsManager.userColor} - onClick={this.handleSendClick} - /> - } color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> - {this._showEditBox && ( -
- this._canInteract && (this._regenInput = e.target.value))} - onKeyDown={this.handleKeyPress} - placeholder="Edit instructions" - /> -
- )} -
+ renderRegenerateEditBox = () => ( +
+ this._canInteract && (this._regenInput = e.target.value))} + onKeyDown={this.handleKeyPress} + placeholder="Edit instructions" + /> +
+ ); + + renderRegenerate = () => ( +
+
+ : } + color={SettingsManager.userColor} + onClick={this.handleSendClick} + /> + } color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} /> + {this._showEditBox ? this.renderRegenerateEditBox() : null}
- ); - } +
+ ); 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/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index d23763eb9..e3345f547 100644 --- a/src/client/views/smartdraw/StickerPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -7,7 +7,7 @@ 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 { emptyFunction, numberRange } from '../../../Utils'; import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { ImageCast, NumCast } from '../../../fields/Types'; @@ -29,6 +29,11 @@ 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. @@ -41,7 +46,34 @@ interface StickerPaletteProps { */ @observer export class StickerPalette extends ObservableReactComponent { - @observable private _paletteMode: 'create' | 'view' = 'view'; + 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(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; @@ -49,7 +81,6 @@ export class StickerPalette extends ObservableReactComponent { - return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); - }; + Contains = (view: DocumentView) => + (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) => { + handleKeyPress = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { - await this.generateDrawings(); + this.generateDrawings(); } }; - @action - setPaletteMode = (mode: 'create' | 'view') => { + setPaletteMode = action((mode: StickerPaletteMode) => { this._paletteMode = mode; - }; + }); - @action - setUserInput = (input: string) => { + setUserInput = action((input: string) => { if (!this._isLoading) this._userInput = input; - }; + }); - @action - setDetail = (detail: number) => { + setDetail = action((detail: number) => { if (this._canInteract) this._opts.complexity = detail; - }; + }); - @action - setColor = (autoColor: boolean) => { + setColor = action((autoColor: boolean) => { if (this._canInteract) this._opts.autoColor = autoColor; - }; + }); - @action - setSize = (size: number) => { + setSize = action((size: number) => { if (this._canInteract) this._opts.size = size; - }; + }); - @action - resetPalette = (changePaletteMode: boolean) => { - if (changePaletteMode) this.setPaletteMode('view'); + resetPalette = action((changePaletteMode: boolean) => { + if (changePaletteMode) this.setPaletteMode(StickerPaletteMode.view); this.setUserInput(''); this.setDetail(5); this.setColor(true); @@ -114,55 +134,31 @@ export class StickerPalette extends ObservableReactComponent { - 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); - if (docView) { - docView.ComponentView?.updateIcon?.(true); - return new Promise(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); - } - return undefined; - } + }); /** * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from * the sticker palette. */ @undoBatch - generateDrawings = action(async () => { + 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; - await Promise.all( - Array.from({ length: 3 }).map((_, i) => { + 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); }) - ); - this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); - this._userInput = ''; - this._isLoading = false; - this._showRegenerate = true; + ).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 @@ -177,7 +173,7 @@ export class StickerPalette extends ObservableReactComponent { + saveDrawing = () => { const cIndex = NumCast(this._props.Document.carousel_index); const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; const docData = focusedDrawing[DocData]; @@ -191,168 +187,160 @@ export class StickerPalette extends ObservableReactComponent this.resetPalette(true)); + }; + + renderCreateInput = () => ( +
+ this.setUserInput(e.target.value)} + placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'} + onKeyDown={this.handleKeyPress} + /> +
+ ); + renderCreateOptions = () => ( +
+
+ Color + this.setColor(!this._opts.autoColor)} + /> +
+
+ Detail + typeof val === 'number' && this.setDetail(val)} + valueLabelDisplay="auto" + /> +
+
+ Size + typeof val === 'number' && this.setSize(val)} + valueLabelDisplay="auto" + /> +
+
+ ); + renderDoc = (doc: Doc, refFunc: (r: DocumentView) => void) => { + return ( + + ); }; + renderPaletteCreate = () => ( + <> + {this.renderCreateInput()} + {this.renderCreateOptions()} + {this.renderDoc(this._props.Document, (r: DocumentView) => { + this._docCarouselView = r; + })} +
+
+
+ + ); + renderPaletteView = () => ( + <> + {this.renderDoc(Doc.MyStickers, (r: DocumentView) => { + this._docView = r; + })} +