From b5944e87f9d4f3149161de4de0d76db486461c76 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
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<StickerPaletteProps> {
+    @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<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 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 (
+            <div className="sticker-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
+                {this._paletteMode === 'view' && (
+                    <>
+                        <DocumentView
+                            ref={r => (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}
+                        />
+                        <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 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>
+                    </>
+                )}
+            </div>
+        );
+    }
+}
+
+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 <eleanor_park@brown.edu>
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<HTMLCanvasElement>(null);
     const canvasBackgroundRef = useRef<HTMLCanvasElement>(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<StickerPaletteProps
         makeObservable(this);
     }
 
+    componentWillUnmount() {
+        this.resetPalette(true);
+    }
+
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(StickerPalette, fieldKey);
     }
@@ -145,19 +149,15 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
     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);
-            }
-        }
+        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 <eleanor_park@brown.edu>
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<string> => {
     }
 };
 
+const gptDescribeImage = async (image: string): Promise<string> => {
+    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<string> => {
     try {
         const response = await openai.chat.completions.create({
@@ -276,11 +311,11 @@ const gptDrawingColor = async (image: string, coords: string[]): Promise<string>
         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<PropertiesViewProps
             10,
             1
         );
+        const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
+            !isNaN(val) && (this.refStrength = val);
+        });
         return (
             <div>
                 {!targetDoc.layout_isSvg && this.containsInkDoc && (
@@ -995,9 +998,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 align="flex-start"
                                 fillWidth
                                 toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => 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')}
                             />
                         </div>
+                        <div className="strength-slider">{strength}</div>
                         <div className="color">
                             <Toggle
                                 text={'Color with GPT'}
@@ -1052,6 +1056,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             doc[DocData].stroke_markerScale = Number(value);
         });
     }
+    @computed get refStrength() { return Number(this.getField('drawing_refStrength') || '50'); } // prettier-ignore
+    set refStrength(value) {
+        this.selectedStrokes.forEach(doc => {
+            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<Partial<collection
         docData.drawingColored = opts.autoColor;
         docData.drawingSize = opts.size;
         docData.drawingData = gptRes;
+        docData.ai_generated = true;
         this._drawingContainer = doc;
         this.addDocument(doc);
         this._batch?.end();
@@ -1992,16 +1993,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }),
                 icon: 'eye',
             });
-        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',
-        });
+        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<FieldViewProps>() {
                 }),
                 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<AntimodeMenuProps> {
         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<object> {
     @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<object> {
      */
     @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<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(() => {
@@ -240,15 +248,12 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     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<object> {
         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<object> {
         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<object> {
                 </div>
                 {this._showOptions && (
                     <div>
-                        <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
+                        <div className="smartdraw-output-options">
+                            <div className="drawing-checkbox">
+                                Generate Ink
+                                <Checkbox
                                     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` } },
+                                        color: 'white',
+                                        '&.Mui-checked': {
+                                            color: SettingsManager.userVariantColor,
+                                        },
                                     }}
-                                    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"
+                                    checked={this._generateDrawing}
+                                    onChange={() => this._canInteract && (this._generateDrawing = !this._generateDrawing)}
                                 />
                             </div>
-                            <div className="size">
-                                Size (in pixels)
-                                <Slider
-                                    className="size-slider"
+                            <div className="image-checkbox">
+                                Generate Image
+                                <Checkbox
                                     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` } },
+                                        color: 'white',
+                                        '&.Mui-checked': {
+                                            color: SettingsManager.userVariantColor,
+                                        },
                                     }}
-                                    min={50}
-                                    max={700}
-                                    step={10}
-                                    size="small"
-                                    value={this._size}
-                                    onChange={action((e, val) => this._canInteract && (this._size = val as number))}
-                                    valueLabelDisplay="auto"
+                                    checked={this._generateImage}
+                                    onChange={() => this._canInteract && (this._generateImage = !this._generateImage)}
                                 />
                             </div>
                         </div>
-                        <div></div>
+                        {this._generateDrawing && (
+                            <div className="smartdraw-svg-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>
+                        )}
                     </div>
                 )}
             </div>
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<StickerPaletteProps
         docData.drawingColored = this._opts.autoColor;
         docData.drawingSize = this._opts.size;
         docData.drawingData = this._gptRes[cIndex];
+        docData.ai_generated = true;
         focusedDrawing.width = this._opts.size;
         docData.x = this._opts.x;
         docData.y = this._opts.y;
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index e73795e41..c5348c7db 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -65,7 +65,11 @@ export default class FireflyManager extends ApiManager {
             })
         );
 
-    generateImage = (prompt: string = 'a realistic illustration of a cat coding') => {
+    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 <eleanor_park@brown.edu>
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 : (
             <Tooltip title={<div className="dash-ai-editor-button">Edit with AI</div>}>
-                <div
-                    className="documentButtonBar-icon"
-                    style={{ color: 'white' }}
-                    onClick={() => {
-                        CalendarManager.Instance.open(this.view0, targetDoc);
-                    }}>
+                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={undoable(() => this.view0?.toggleAIEditor(), 'toggle AI editor')}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
                 </div>
             </Tooltip>
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<P> extends ObservableReactComponent<React
     search?: (str: string, bwd?: boolean, clear?: boolean) => 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<Partial<collection
         const docData = doc[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
         docData._width = opts.size;
-        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';
         this._drawingContainer = doc;
         this.addDocument(doc);
         this._batch?.end();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 048b92c71..ab58023e3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -123,6 +123,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _componentView: Opt<ViewBoxInterface<FieldViewProps>> = undefined; // needs to be accessed from DocumentView wrapper class
     @observable _animateScaleTime: Opt<number> = 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<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => 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<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
+    rpw = () => 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<FieldViewProps & Document
                 className="documentView-contentsView"
                 style={{
                     pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                    height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                    width: this._showAIEditor ? this.rpw() : undefined,
+                    height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                    justifySelf: 'center',
                 }}>
                 <DocumentContentsView
                     {...this._props}
@@ -728,7 +737,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     pointerEvents={this.contentPointerEvents}
                     setContentViewBox={this.setContentView}
                     childFilters={this.childFilters}
-                    PanelHeight={this.panelHeight}
+                    PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
+                    PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
                     setHeight={this.setHeight}
                     isContentActive={this.isContentActive}
                     ScreenToLocalTransform={this.screenToLocalContent}
@@ -737,6 +747,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     setTitleFocus={this.setTitleFocus}
                     hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                 />
+                {this._showAIEditor && (this._componentView?.componentAIView?.(this.rph()) ?? null)}
             </div>
         );
     }
@@ -1286,6 +1297,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
     };
 
+    @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<FieldViewProps>() {
                 }),
                 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<FieldViewProps>() {
         );
     }
 
+    @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 (
+            <div className="imageBox-aiView" style={{ top: top, width: NumCast(this.Document.width), height: NumCast(this.Document.width) - top }}>
+                Edit Image with AI
+                {showRegenerate && (
+                    <div className="imageBox-aiView-regenerate-container">
+                        <text className="imageBox-aiView-subtitle">Regenerate AI Image</text>
+                        <div className="imageBox-aiView-regenerate">
+                            <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="Prompt (Optional)"
+                            />
+                            <Button
+                                text="Regenerate"
+                                type={Type.SEC}
+                                // style={{ alignSelf: 'flex-end' }}
+                                icon={this._regenerateLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                onClick={action(async () => {
+                                    this._regenerateLoading = true;
+                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput);
+                                    this._regenerateLoading = false;
+                                    this._regenInput = '';
+                                })}
+                            />
+                            <Button
+                                // style={{ alignSelf: 'flex-end' }}
+                                text="Get Variations"
+                                type={Type.SEC}
+                                // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                // onClick={this.handleSendClick}
+                            />
+                        </div>
+                    </div>
+                )}
+                <div className="imageBox-aiView-options-container">
+                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Image Options </text>}
+                    <div className="imageBox-aiView-options">
+                        <Button
+                            type={Type.TERT}
+                            text="Get Text"
+                            icon={<FontAwesomeIcon icon="font" />}
+                            color={SettingsManager.userBackgroundColor}
+                            iconPlacement="right"
+                            onClick={() => {
+                                Networking.PostToServer('/queryFireflyImageText', {
+                                    file: (file => {
+                                        const ext = extname(file);
+                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                                }).then(text => alert(text));
+                            }}
+                        />
+                        <Button
+                            type={Type.TERT}
+                            text="Generative Fill"
+                            icon={<FontAwesomeIcon icon="fill" />}
+                            color={SettingsManager.userBackgroundColor}
+                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={action(() => {
+                                ImageEditorData.Open = true;
+                                ImageEditorData.Source = (field && this.choosePath(field.url)) || '';
+                                ImageEditorData.AddDoc = this._props.addDocument;
+                                ImageEditorData.RootDoc = this.Document;
+                            })}
+                        />
+                        <Button
+                            type={Type.TERT}
+                            text="Expand"
+                            icon={<FontAwesomeIcon icon="expand" />}
+                            color={SettingsManager.userBackgroundColor}
+                            // icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={() => {
+                                Networking.PostToServer('/expandImage', {
+                                    prompt: 'sunny skies',
+                                    file: (file => {
+                                        const ext = extname(file);
+                                        return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
+                                    })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
+                                }).then((info: Upload.ImageInformation) => {
+                                    const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
+                                    DocUtils.assignImageInfo(info, img);
+                                    this._props.addDocTab(img, OpenWhere.addRight);
+                                });
+                            }}
+                        />
+                    </div>
+                </div>
+            </div>
+        );
+    };
+
     @computed get annotationLayer() {
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this._props.PanelHeight() }} ref={this._annotationLayer} />;
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<AntimodeMenuProps> {
         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<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;
@@ -144,14 +144,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 };
     };
 
     /**
@@ -205,8 +205,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         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<object> {
                 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<object> {
                 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<StickerPaletteProps
     }
 
     /**
-     * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+     * Calls the draw with AI functions in SmartDrawHandler to allow users to generate drawings straight from
      * the sticker palette.
      */
     @undoBatch
     generateDrawings = action(async () => {
         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<StickerPaletteProps
         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.ai_generated = true;
+        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;
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index cb5d7c1da..6daa5840e 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -70,7 +70,6 @@ export default class FireflyManager extends ApiManager {
         );
 
     generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => {
-        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 <zzzman@gmail.com>
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<FieldViewProps>() {
             <div className="imageBox-aiView-history">
                 {this._prevImgs.map(img => (
                     <img
+                        key={img.pathname}
                         className="imageBox-aiView-img"
                         src={img.href}
                         onClick={() => {
@@ -583,7 +583,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 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<object> {
         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<object> {
      * @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<object> {
     /**
      * 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) {
@@ -401,7 +400,108 @@ 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
                 className="smart-draw-handler"
@@ -444,154 +544,64 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 </div>
                 {this._showOptions && (
                     <div>
-                        <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>
-                        {this._generateDrawing && (
-                            <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>
-                        )}
-                        {this._generateImage && (
-                            <div className="smartdraw-options-container">
-                                Image Options
-                                <div className="smartdraw-dimensions">
-                                    <RadioGroup row defaultValue="square" sx={{ alignItems: 'center' }}>
-                                        <FormControlLabel sx={{ width: '40%' }} value="square" control={<Radio />} onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Square)} label="Square" />
-                                        <FormControlLabel sx={{ width: '40%' }} value="landscape" control={<Radio />} onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Landscape)} label="Landscape" />
-                                        <FormControlLabel sx={{ width: '40%' }} value="portrait" control={<Radio />} onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Portrait)} label="Portrait" />
-                                        <FormControlLabel sx={{ width: '40%' }} value="widescreen" control={<Radio />} onChange={() => this._canInteract && (this._imgDims = FireflyImageDimensions.Widescreen)} label="Widescreen" />
-                                    </RadioGroup>
-                                </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/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<StickerPaletteProps> {
-    @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<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;
@@ -49,7 +81,6 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
     @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);
@@ -60,51 +91,40 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         this.resetPalette(true);
     }
 
-    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));
-    };
+    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<StickerPaletteProps
         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<ImageField | undefined>(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<StickerPaletteProps
      * 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 () => {
+    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<StickerPaletteProps
         focusedDrawing.width = this._opts.size;
         docData.x = this._opts.x;
         docData.y = this._opts.y;
-        await StickerPalette.addToPalette(focusedDrawing);
-        this.resetPalette(true);
+        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 === 'view' && (
-                    <>
-                        <DocumentView
-                            ref={r => (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}
-                        />
-                        <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 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>
-                    </>
-                )}
+                {this._paletteMode === StickerPaletteMode.view ? this.renderPaletteView() : null}
+                {this._paletteMode === StickerPaletteMode.create ? this.renderPaletteCreate() : null}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From eabaae11dc5c5e1de463d2903c3c8e9407506c92 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Jan 2025 10:37:09 -0500
Subject: from last

---
 src/client/views/smartdraw/StickerPalette.tsx | 4 ++--
 src/server/server_Initialization.ts           | 3 +--
 2 files changed, 3 insertions(+), 4 deletions(-)

(limited to 'src/client/views/smartdraw/StickerPalette.tsx')

diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
index e3345f547..d5307974f 100644
--- a/src/client/views/smartdraw/StickerPalette.tsx
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -1,6 +1,6 @@
+import { Button } from '@dash/components';
 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';
@@ -22,8 +22,8 @@ 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';
+import './StickerPalette.scss';
 
 interface StickerPaletteProps {
     Document: Doc;
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 1bd495965..12c5319b7 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -115,7 +115,7 @@ function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
 function proxyServe(req: any, requrl: string, response: any) {
-    // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
+    // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports
     const htmlBodyMemoryStream = new (require('memorystream'))();
     let wasinBrFormat = false;
     const sendModifiedBody = () => {
@@ -189,7 +189,6 @@ function proxyServe(req: any, requrl: string, response: any) {
                 res.headers['x-permitted-cross-domain-policies'] = 'all';
                 res.headers['x-frame-options'] = '';
                 res.headers['content-security-policy'] = '';
-                // eslint-disable-next-line no-multi-assign
                 response.headers = response._headers = res.headers;
             })
             .on('end', sendModifiedBody)
-- 
cgit v1.2.3-70-g09d2

