From 2577ff5a88be04a840c4ac55360265b35d08fe19 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 4 Dec 2024 23:00:55 -0500
Subject: created drawing fill handler

---
 src/client/views/smartdraw/DrawingFillHandler.tsx | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 src/client/views/smartdraw/DrawingFillHandler.tsx

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

diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
new file mode 100644
index 000000000..e69de29bb
-- 
cgit v1.2.3-70-g09d2


From 0066635b2a9ae5fde15defd9dd76c6d7dc1d8959 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 5 Dec 2024 00:22:08 -0500
Subject: added image to blob logic and firefly api call in drawingFillHandler

---
 src/client/views/smartdraw/DrawingFillHandler.tsx | 48 ++++++++++++
 src/server/ApiManagers/FireflyManager.ts          | 90 ++++++++++++-----------
 2 files changed, 96 insertions(+), 42 deletions(-)

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

diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index e69de29bb..508ee557d 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -0,0 +1,48 @@
+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';
+
+@observer
+export class DrawingFillHandler extends ObservableReactComponent<object> {
+    static Instance: DrawingFillHandler;
+
+    constructor(props: object) {
+        super(props);
+        makeObservable(this);
+        DrawingFillHandler.Instance = this;
+    }
+
+    @action
+    drawingToImage = async (drawing: Doc, prompt: string) => {
+        const imageField = await DocumentView.GetDocImage(drawing);
+        if (!imageField) return;
+        const image = new Image();
+        image.src = imageField.url?.href;
+        await new Promise<void>((resolve, reject) => {
+            image.onload = () => resolve();
+            image.onerror = () => reject(new Error('Error loading image'));
+        });
+
+        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/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 29aed2c54..55fa1e461 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -14,43 +14,43 @@ export default class FireflyManager extends ApiManager {
             console.error('Error:', error);
             return '';
         });
-    askFirefly = (prompt: string = 'a realistic illustration of a cat coding') => {
-        const fetched = this.getBearerToken().then(response =>
-            response.json().then((data: { access_token: string }) =>
-                fetch('https://firefly-api.adobe.io/v3/images/generate', {
-                    method: 'POST',
-                    headers: [
-                        ['Content-Type', 'application/json'],
-                        ['Accept', 'application/json'],
-                        ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
-                        ['Authorization', `Bearer ${data.access_token}`],
-                    ],
-                    body: `{  "prompt": "${prompt}" }`,
-                })
-                    .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
-                    .catch(error => {
-                        console.error('Error:', error);
-                        return '';
-                    })
-            )
-        );
-        return fetched;
-    };
+    // askFirefly = (prompt: string = 'a realistic illustration of a cat coding') => {
+    //     const fetched = this.getBearerToken().then(response =>
+    //         response.json().then((data: { access_token: string }) =>
+    //             fetch('https://firefly-api.adobe.io/v3/images/generate', {
+    //                 method: 'POST',
+    //                 headers: [
+    //                     ['Content-Type', 'application/json'],
+    //                     ['Accept', 'application/json'],
+    //                     ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
+    //                     ['Authorization', `Bearer ${data.access_token}`],
+    //                 ],
+    //                 body: `{  "prompt": "${prompt}" }`,
+    //             })
+    //                 .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
+    //                 .catch(error => {
+    //                     console.error('Error:', error);
+    //                     return '';
+    //                 })
+    //         )
+    //     );
+    //     return fetched;
+    // };
 
-    askFireflyStructure = (prompt: string = 'a realistic illustration of a cat coding', strength: number, imageReference: ) => {
+    askFirefly = (prompt: string = 'a realistic illustration of a cat coding', uploadId: string, strength: number) => {
         const fetched = this.getBearerToken().then(response =>
             response.json().then((data: { access_token: string }) => {
-                const body = {
+                const body: any = {
                     prompt: prompt,
                     structure: {
                         strength: strength,
                         imageReference: {
                             source: {
-                                uploadId:
-                            }
-                        }
-                    }
-                }
+                                uploadId: uploadId,
+                            },
+                        },
+                    },
+                };
                 return fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
@@ -65,13 +65,13 @@ export default class FireflyManager extends ApiManager {
                     .catch(error => {
                         console.error('Error:', error);
                         return '';
-                    })
-                })
+                    });
+            })
         );
         return fetched;
     };
 
-    uploadImageToFirefly = (image: File | Blob) => {
+    uploadImageToFirefly = (image: Blob) => {
         const fetched = this.getBearerToken().then(response =>
             response.json().then((data: { access_token: string }) =>
                 fetch('https://firefly-api.adobe.io/v3/uploads', {
@@ -79,29 +79,35 @@ export default class FireflyManager extends ApiManager {
                     headers: [
                         ['Content-Type', image.type],
                         ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
-                        ['Authorization', `Bearer ${data.access_token}`], // You can replace this with a dynamic token if needed
+                        ['Authorization', `Bearer ${data.access_token}`],
                     ],
                     body: image,
                 })
-        .then(response => response.json())
-        .then(data => data.uploadId) // Extract the uploadId from the response
-        .catch(error => {
-            console.error('Error uploading image:', error);
-            return '';
-        });
+                    .then(response => response.json())
+                    .then(data => data.uploadId) // Extract the uploadId from the response
+                    .catch(error => {
+                        console.error('Error:', error);
+                        return '';
+                    })
+            )
+        );
+        return fetched;
     };
 
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImage',
-            secureHandler: ({ req, res }) =>
-                this.askFirefly(req.body.prompt).then(fire =>
+            secureHandler: async ({ req, res }) => {
+                const { prompt, imageBlob, strength = 0.5 } = req.body;
+                const uploadId = imageBlob ? await this.uploadImageToFirefly(imageBlob) : null;
+                this.askFirefly(prompt, uploadId, strength).then(fire =>
                     DashUploadUtils.UploadImage(JSON.parse(fire).url).then(info => {
                         if (info instanceof Error) _invalid(res, info.message);
                         else _success(res, info.accessPaths.agnostic.client);
                     })
-                ),
+                );
+            },
         });
     }
 }
-- 
cgit v1.2.3-70-g09d2


From 80c4e34efd5ce34dfa4c3f0d1e55fd2ecf9d6a4d Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 8 Dec 2024 00:59:21 -0500
Subject: frontend and image creation working for drawingToImage()

---
 src/client/views/PropertiesView.tsx               | 80 ++++++++++++-----------
 src/client/views/smartdraw/DrawingFillHandler.tsx | 60 +++++++++--------
 src/client/views/smartdraw/SmartDrawHandler.tsx   | 29 ++++----
 3 files changed, 87 insertions(+), 82 deletions(-)

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

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 7e8087808..4e12e0b2d 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -3,7 +3,6 @@ import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-soli
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox, Tooltip } from '@mui/material';
 import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components';
-import { Property } from 'csstype';
 import { concat } from 'lodash';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -45,6 +44,7 @@ import { StyleProviderFuncType } from './nodes/FieldView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
 import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
+import { DrawingFillHandler } from './smartdraw/DrawingFillHandler';
 
 interface PropertiesViewProps {
     width: number;
@@ -141,7 +141,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get isText() {
-        return [DocumentType.RTF, DocumentType.EQUATION, DocumentType.LABEL].includes(this.selectedDoc?.type as DocumentType);
+        return this.selectedDoc?.type === DocumentType.RTF;
     }
     @computed get isInk() {
         return this.selectedDoc?.type === DocumentType.INK;
@@ -830,8 +830,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set shapeWid(value)  { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
     set shapeHgt(value)  { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
-    @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width, 1); } // prettier-ignore
-    set strokeThk(value) { this.selectedStrokes.forEach(doc => { doc[DocData].stroke_width = Math.round(value * 100) / 100; }); } // prettier-ignore
+    @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width); } // prettier-ignore
+    set strokeThk(value) {
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_width = Math.round(value * 100) / 100;
+        });
+    }
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
@@ -981,19 +985,35 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return (
             <div>
                 {!targetDoc.layout_isSvg && this.containsInkDoc && (
-                    <div className="color">
-                        <Toggle
-                            text={'Color with GPT'}
-                            color={SettingsManager.userColor}
-                            icon={<FontAwesomeIcon icon="fill-drip" />}
-                            iconPlacement="left"
-                            align="flex-start"
-                            fillWidth
-                            toggleType={ToggleType.BUTTON}
-                            onClick={undoable(() => {
-                                SmartDrawHandler.Instance.colorWithGPT(targetDoc);
-                            }, 'colorWithGPT')}
-                        />
+                    <div>
+                        <div className="drawing-to-image">
+                            <Toggle
+                                text={'Create Image'}
+                                color={SettingsManager.userColor}
+                                icon={<FontAwesomeIcon icon="fill-drip" />}
+                                iconPlacement="left"
+                                align="flex-start"
+                                fillWidth
+                                toggleType={ToggleType.BUTTON}
+                                onClick={undoable(() => {
+                                    DrawingFillHandler.drawingToImage(targetDoc, 'fill in the details of this image');
+                                }, 'createImage')}
+                            />
+                        </div>
+                        <div className="color">
+                            <Toggle
+                                text={'Color with GPT'}
+                                color={SettingsManager.userColor}
+                                icon={<FontAwesomeIcon icon="fill-drip" />}
+                                iconPlacement="left"
+                                align="flex-start"
+                                fillWidth
+                                toggleType={ToggleType.BUTTON}
+                                onClick={undoable(() => {
+                                    SmartDrawHandler.Instance.colorWithGPT(targetDoc);
+                                }, 'colorWithGPT')}
+                            />
+                        </div>
                     </div>
                 )}
                 <div className="smooth">
@@ -1022,13 +1042,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             doc[DocData].stroke_dash = value ? this._lastDash : undefined;
         });
     }
-    @computed get lineCapStk() { return (this.getField('stroke_lineCap') || 'round' ) as Property.StrokeLinecap; } // prettier-ignore
-    set lineCapStk(value) {
-        this.selectedStrokes.forEach(doc => {
-            doc[DocData].stroke_lineCap = value;
-        });
-    }
-    @computed get widthStk() { return this.getField('stroke') || '1'; } // prettier-ignore
+    @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
     set widthStk(value) {
         this.selectedStrokes.forEach(doc => {
             doc[DocData].stroke_width = Number(value);
@@ -1130,6 +1144,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     <div className="arrows-head">
                         <div className="arrows-head-title">Arrow Head: </div>
                         <input
+                            key="markHead"
                             className="arrows-head-input"
                             type="checkbox"
                             checked={this.markHead !== ''}
@@ -1139,25 +1154,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     <div className="arrows-tail">
                         <div className="arrows-tail-title">Arrow End: </div>
                         <input
+                            key="markTail"
                             className="arrows-tail-input"
                             type="checkbox"
                             checked={this.markTail !== ''}
-                            onChange={undoable(action(() => { this.markTail = this.markTail ? '' : 'arrow'; }) ,"change arrow tail")}
+                            onChange={undoable(
+                                action(() => { this.markTail = this.markTail ? '' : 'arrow'; }) ,"change arrow tail"
+                            )}
                         />
                     </div>
                 </div>
-                <div className="arrows">
-                    {["butt", "round", "square"].map(cap => 
-                    <div className="arrows-tail" key={cap}>
-                        <div className="arrows-tail-title">{cap}</div>
-                        <input
-                            className="arrows-tail-input"
-                            type="checkbox"
-                            checked={this.lineCapStk === cap}
-                            onChange={undoable(action(() => {  this.lineCapStk = cap as Property.StrokeLinecap;  }), `change lineCap ${cap}`)}
-                        />
-                    </div>)}
-                </div>
                 <div className="dashed">
                     <div className="dashed-title">Dashed Line:</div>
                     <input key="markHead" className="dashed-input" type="checkbox" checked={this.dashdStk === '2'} onChange={this.changeDash} />
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 508ee557d..c3c762181 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -12,37 +12,39 @@ import { ImageUtility } from '../nodes/imageEditor/imageEditorUtils/ImageHandler
 import { OpenWhere } from '../nodes/OpenWhere';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 
-@observer
-export class DrawingFillHandler extends ObservableReactComponent<object> {
-    static Instance: DrawingFillHandler;
-
-    constructor(props: object) {
-        super(props);
-        makeObservable(this);
-        DrawingFillHandler.Instance = this;
-    }
-
-    @action
-    drawingToImage = async (drawing: Doc, prompt: string) => {
+export class DrawingFillHandler {
+    static drawingToImage = async (drawing: Doc, prompt: string) => {
         const imageField = await DocumentView.GetDocImage(drawing);
         if (!imageField) return;
-        const image = new Image();
-        image.src = imageField.url?.href;
-        await new Promise<void>((resolve, reject) => {
-            image.onload = () => resolve();
-            image.onerror = () => reject(new Error('Error loading image'));
-        });
+        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 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/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index e962291f6..23ab7657f 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -13,7 +13,6 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkData, InkField, InkTool } from '../../../fields/InkField';
 import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
-import { Networking } from '../../Network';
 import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { SettingsManager } from '../../util/SettingsManager';
@@ -22,8 +21,7 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { InkingStroke } from '../InkingStroke';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveInkColor, ActiveInkDash, ActiveInkFillColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-import { OpenWhere } from '../nodes/OpenWhere';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
 import './SmartDrawHandler.scss';
 
 export interface DrawingOptions {
@@ -238,21 +236,20 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * Calls GPT API to create a drawing based on user input.
      */
     @action
-    drawWithGPT = (startPt: { X: number; Y: number }, prompt: string, complexity: number, size: number, autoColor: boolean) => {
-        if (prompt === '') return;
-        this._lastInput = { text: prompt, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
-
-        Networking.PostToServer('/queryFireflyImage', { prompt }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, { title: prompt }), OpenWhere.addRight));
-
-        const result = gptAPICall(`"${prompt}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true).then(res =>
-            this.parseSvg(res, startPt, false, autoColor).then(strokeData => {
-                const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
-            })
-        );
+    drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
+        if (input === '') return;
+        this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+        const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
+        if (!res) {
+            console.error('GPT call failed');
+            return;
+        }
+        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._errorOccurredOnce = false;
-        return result;
+        return strokeData;
     };
 
     /**
-- 
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/DrawingFillHandler.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 0034a4b7d1bbaf5c6909d76b1a2dac85054fa4ff Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 14 Dec 2024 14:27:03 -0500
Subject: preparing to merge

---
 src/client/views/smartdraw/DrawingFillHandler.tsx |  6 ++---
 src/server/ApiManagers/FireflyManager.ts          | 28 +++--------------------
 2 files changed, 6 insertions(+), 28 deletions(-)

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

diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 8f71b150d..6f111e95b 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -23,9 +23,9 @@
 //             const response = await fetch(hrefComplete);
 //             const blob: Blob = await response.blob();
 //             const strength: number = 100;
-//             const img = await Networking.PostToServer('/queryFireflyImage', { prompt, blob, strength });
+//             const img = await Networking.PostToServer('/oldQueryFireflyImage', { 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));
+//             // Networking.PostToServer('/oldQueryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
 //         } catch (error) {
 //             console.error('Error fetching image:', error);
 //             return;
@@ -44,7 +44,7 @@
 //         //     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));
+//         // Networking.PostToServer('/oldQueryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
 //         // };
 //     };
 // }
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 55fa1e461..d9a1ef621 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -14,30 +14,8 @@ export default class FireflyManager extends ApiManager {
             console.error('Error:', error);
             return '';
         });
-    // askFirefly = (prompt: string = 'a realistic illustration of a cat coding') => {
-    //     const fetched = this.getBearerToken().then(response =>
-    //         response.json().then((data: { access_token: string }) =>
-    //             fetch('https://firefly-api.adobe.io/v3/images/generate', {
-    //                 method: 'POST',
-    //                 headers: [
-    //                     ['Content-Type', 'application/json'],
-    //                     ['Accept', 'application/json'],
-    //                     ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
-    //                     ['Authorization', `Bearer ${data.access_token}`],
-    //                 ],
-    //                 body: `{  "prompt": "${prompt}" }`,
-    //             })
-    //                 .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
-    //                 .catch(error => {
-    //                     console.error('Error:', error);
-    //                     return '';
-    //                 })
-    //         )
-    //     );
-    //     return fetched;
-    // };
 
-    askFirefly = (prompt: string = 'a realistic illustration of a cat coding', uploadId: string, strength: number) => {
+    askFireflyOld = (prompt: string = 'a realistic illustration of a cat coding', uploadId: string, strength: number) => {
         const fetched = this.getBearerToken().then(response =>
             response.json().then((data: { access_token: string }) => {
                 const body: any = {
@@ -97,11 +75,11 @@ export default class FireflyManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
-            subscription: '/queryFireflyImage',
+            subscription: '/oldQueryFireflyImage',
             secureHandler: async ({ req, res }) => {
                 const { prompt, imageBlob, strength = 0.5 } = req.body;
                 const uploadId = imageBlob ? await this.uploadImageToFirefly(imageBlob) : null;
-                this.askFirefly(prompt, uploadId, strength).then(fire =>
+                this.askFireflyOld(prompt, uploadId, strength).then(fire =>
                     DashUploadUtils.UploadImage(JSON.parse(fire).url).then(info => {
                         if (info instanceof Error) _invalid(res, info.message);
                         else _success(res, info.accessPaths.agnostic.client);
-- 
cgit v1.2.3-70-g09d2


From b7105063030ee29ba644d8fbb5878c8ee41fd389 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 16 Dec 2024 11:52:42 -0500
Subject: fixed api for creating an image using another image as a structure
 reference.

---
 src/client/views/ContextMenuItem.tsx               |  10 +-
 src/client/views/PropertiesView.tsx                |   4 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   3 -
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 src/client/views/nodes/ImageBox.tsx                |   3 +-
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  70 ++++-------
 src/server/ApiManagers/FireflyManager.ts           | 132 ++++++++-------------
 7 files changed, 82 insertions(+), 144 deletions(-)

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

diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 6f8f41bdd..218718b18 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { SnappingManager } from '../util/SnappingManager';
@@ -26,7 +26,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     _hoverTimeout?: NodeJS.Timeout;
     _overPosY = 0;
     _overPosX = 0;
-    @observable _items: ContextMenuProps[] = [];
+    @observable.shallow _items: ContextMenuProps[] = [];
     @observable _overItem = false;
 
     constructor(props: ContextMenuProps & { selected?: boolean }) {
@@ -34,8 +34,8 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
         makeObservable(this);
     }
 
-    componentDidMount() {
-        runInAction(() => this._items.push(...(this._props.subitems ?? [])));
+    @computed get items() {
+        return this._items.concat(this._props.subitems ?? []);
     }
 
     handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
@@ -91,7 +91,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps &
     };
 
     render() {
-        const submenu = this._items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
+        const submenu = this.items.map(prop => <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} />);
         return this.props.event || this._props.noexpand ? this.renderItem(submenu) : <div className="contextMenu-inlineMenu">{submenu}</div>;
     }
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 4e12e0b2d..aefdeee17 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -995,9 +995,7 @@ 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, 'fill in the details of this image'), 'createImage')}
                             />
                         </div>
                         <div className="color">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 79aad0ef2..272c13546 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -102,7 +102,6 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
             replica: '',
         });
     });
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
@@ -272,7 +271,6 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
         payload: pivotColumnGroups.get(key)?.filters,
     }));
     groupNames.push(...dividers);
-    // eslint-disable-next-line no-use-before-define
     return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []);
 }
 
@@ -347,7 +345,6 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc:
         if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
             groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
         }
-        // eslint-disable-next-line no-use-before-define
         layoutDocsAtTime(keyDocs, key);
     });
     if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4bfa7fc92..8519cda3c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,6 +52,7 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails/PresEnums';
 import SpringAnimation from './trails/SlideEffect';
 import { SpringType, springMappings } from './trails/SpringUtils';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export interface DocumentViewProps extends FieldViewSharedProps {
     hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
@@ -551,6 +552,7 @@ 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' });
@@ -1072,7 +1074,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static GetDocImage(doc: Doc) {
         return DocumentView.getDocumentView(doc)
             ?.ComponentView?.updateIcon?.()
-            .then(() => ImageCast(DocCast(doc).icon));
+            .then(() => ImageCast(doc?.icon, ImageCast(doc[Doc.LayoutFieldKey(doc)])));
     }
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d847b7940..03d417e4e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -40,7 +40,6 @@ import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
-import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -361,6 +360,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
     };
 
+    updateIcon = () => new Promise<void>(res => res());
+
     choosePath = (url: URL) => {
         if (!url?.href) return '';
         const lower = url.href.toLowerCase();
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 6f111e95b..32416cec5 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,50 +1,22 @@
-// 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 { Doc } from '../../../fields/Doc';
+import { ImageCast } from '../../../fields/Types';
+import { Upload } from '../../../server/SharedMediaTypes';
+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 = 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('/oldQueryFireflyImage', { prompt, blob, strength });
-//             DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight);
-//             // Networking.PostToServer('/oldQueryFireflyImage', { 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('/oldQueryFireflyImage', { prompt, blob, strength }).then(img => DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(img, {}), OpenWhere.addRight));
-//         // };
-//     };
-// }
+export class DrawingFillHandler {
+    static drawingToImage = (drawing: Doc, prompt: string) =>
+        DocumentView.GetDocImage(drawing)?.then(imageField => {
+            if (imageField) {
+                const { href } = ImageCast(imageField).url;
+                const hrefParts = href.split('.');
+                const structureUrl = `${hrefParts[0]}_o.${hrefParts[1]}`;
+                const strength: number = 100;
+                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/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 73cf94206..c84e9e8fa 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,21 +20,10 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    askFireflyOld = (prompt: string = 'a realistic illustration of a cat coding', uploadId: string, strength: number) => {
-        const fetched = this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) => {
-                const body: any = {
-                    prompt: prompt,
-                    structure: {
-                        strength: strength,
-                        imageReference: {
-                            source: {
-                                uploadId: uploadId,
-                            },
-                        },
-                    },
-                };
-                return fetch('https://firefly-api.adobe.io/v3/images/generate', {
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', structureUrl: string, strength: number) =>
+        this.getBearerToken().then(response =>
+            response?.json().then((data: { access_token: string }) =>
+                fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
                         ['Content-Type', 'application/json'],
@@ -42,40 +31,40 @@ export default class FireflyManager extends ApiManager {
                         ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
                         ['Authorization', `Bearer ${data.access_token}`],
                     ],
-                    body: JSON.stringify(body),
+                    body: JSON.stringify({
+                        prompt,
+                        structure: !structureUrl
+                            ? undefined
+                            : {
+                                  strength,
+                                  imageReference: {
+                                      source: { url: structureUrl },
+                                  },
+                              },
+                    }),
                 })
-                    .then(response => response.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
+                    .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
                     .catch(error => {
                         console.error('Error:', error);
                         return '';
+                    })
+            )
+        );
+
+    uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
+        new Promise<string>((res, rej) =>
+            fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
+                if (err) {
+                    console.log('Error: ', err);
+                    rej();
+                } else {
+                    dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }).then(response => {
+                        dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => res(link.result.link));
                     });
+                }
             })
         );
-        return fetched;
-    };
 
-    uploadImageToFirefly = (image: Blob) => {
-        const fetched = this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) =>
-                fetch('https://firefly-api.adobe.io/v3/uploads', {
-                    method: 'POST',
-                    headers: [
-                        ['Content-Type', image.type],
-                        ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''],
-                        ['Authorization', `Bearer ${data.access_token}`],
-                    ],
-                    body: image,
-                })
-                    .then(response => response.json())
-                    .then(data => data.uploadId) // Extract the uploadId from the response
-                    .catch(error => {
-                        console.error('Error:', error);
-                        return '';
-                    })
-            )
-        );
-        return fetched;
-    };
     generateImage = (prompt: string = 'a realistic illustration of a cat coding') => {
         const fetched = this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
@@ -222,17 +211,16 @@ export default class FireflyManager extends ApiManager {
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
-            subscription: '/oldQueryFireflyImage',
-            secureHandler: async ({ req, res }) => {
-                const { prompt, imageBlob, strength = 0.5 } = req.body;
-                const uploadId = imageBlob ? await this.uploadImageToFirefly(imageBlob) : null;
-                this.askFireflyOld(prompt, uploadId, strength).then(fire =>
-                    DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                        if (info instanceof Error) _invalid(res, info.message);
-                        else _success(res, info);
-                    })
-                );
-            },
+            subscription: '/queryFireflyImageFromStructure',
+            secureHandler: async ({ req, res }) =>
+                this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
+                    this.generateImageFromStructure(req.body.prompt, uploadUrl, req.body.strength).then(fire =>
+                        DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
+                            if (info instanceof Error) _invalid(res, info.message);
+                            else _success(res, info);
+                        })
+                    )
+                ),
         });
         register({
             method: Method.POST,
@@ -263,36 +251,16 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/expandImage',
             secureHandler: ({ req, res }) =>
-                new Promise<void>((resolve, reject) => {
-                    const dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN });
-                    fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(req.body.file)}`), undefined, (err, contents) => {
-                        if (err) {
-                            console.log('Error: ', err);
-                            reject();
-                        } else {
-                            dbx.filesUpload({ path: `/Apps/browndash/${path.basename(req.body.file)}`, contents })
-                                .then(response => {
-                                    dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => {
-                                        console.log(link.result);
-                                        this.expandImage(link.result.link, req.body.prompt).then(text => {
-                                            if (text.error_code) _error(res, text.message);
-                                            else
-                                                DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
-                                                    if (info instanceof Error) _invalid(res, info.message);
-                                                    else _success(res, info);
-                                                    resolve();
-                                                });
-                                        });
-                                    });
-                                })
-                                .catch(uploadErr => {
-                                    console.log(uploadErr);
-                                    _error(res, 'upload to dropbox failed');
-                                    reject();
-                                });
-                        }
-                    });
-                }),
+                this.uploadImageToDropbox(req.body.file).then(uploadUrl =>
+                    this.expandImage(uploadUrl, req.body.prompt).then(text => {
+                        if (text.error_code) _error(res, text.message);
+                        else
+                            DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
+                                if (info instanceof Error) _invalid(res, info.message);
+                                else _success(res, info);
+                            });
+                    })
+                ),
         });
     }
 }
-- 
cgit v1.2.3-70-g09d2


From 337fbd9c4ce14a548e8b897c2cbb23d5de2a892f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 17 Dec 2024 13:46:11 -0500
Subject: fixed  DrawingFillHandler o reconstruct structureUrl properly.

---
 src/client/views/smartdraw/DrawingFillHandler.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 32416cec5..48e71bc9f 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -12,7 +12,7 @@ export class DrawingFillHandler {
             if (imageField) {
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
-                const structureUrl = `${hrefParts[0]}_o.${hrefParts[1]}`;
+                const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
                 const strength: number = 100;
                 Networking.PostToServer('/queryFireflyImageFromStructure', { prompt, structureUrl, strength }).then((info: Upload.ImageInformation) => 
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {}), OpenWhere.addRight)) // prettier-ignore
-- 
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/DrawingFillHandler.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 383b0487d5268bd860e514feddf09f4f3eb2fe3f Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 2 Jan 2025 01:13:50 -0500
Subject: made drawing fill automatically size images

---
 src/client/util/bezierFit.ts                       |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 src/client/views/nodes/ImageBox.tsx                |   5 +-
 .../imageEditor/imageEditingUtils/ImageHandler.ts  | 312 ---------------------
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  18 +-
 src/client/views/smartdraw/FireflyConstants.ts     |  20 ++
 src/client/views/smartdraw/SmartDrawHandler.scss   |   3 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  34 +--
 src/server/ApiManagers/FireflyManager.ts           |   7 +-
 9 files changed, 51 insertions(+), 351 deletions(-)
 delete mode 100644 src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts
 create mode 100644 src/client/views/smartdraw/FireflyConstants.ts

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

diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 84b27e84c..7ef370d48 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -703,6 +703,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
                     coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
+                    coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
                     lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) };
                 } else if (match[0].startsWith('C')) {
                     coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4bccdd286..ef0b80720 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1995,7 +1995,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             });
         this.layoutDoc.drawingData != undefined &&
             optionItems.push({
-                description: 'Show Drawing Editor',
+                description: 'Regenerate AI Drawing',
                 event: action(() => {
                     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
                     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8f6a90e61..7ce429f0f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -355,9 +355,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             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();
+                    event: action(e => {
+                        !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(e?.x || 0, e?.y || 0) : SmartDrawHandler.Instance.hideRegenerate();
                     }),
                     icon: 'pen-to-square',
                 });
diff --git a/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts b/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts
deleted file mode 100644
index 514e8a94f..000000000
--- a/src/client/views/nodes/imageEditor/imageEditingUtils/ImageHandler.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-import { RefObject } from 'react';
-import { bgColor, canvasSize } from '../ImageEditorUtils/imageEditorConstants';
-
-export interface APISuccess {
-    status: 'success';
-    urls: string[];
-}
-
-export interface APIError {
-    status: 'error';
-    message: string;
-}
-
-export class ImageUtility {
-    /**
-     *
-     * @param canvas Canvas to convert
-     * @returns Blob of canvas
-     */
-    static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> =>
-        new Promise(resolve => {
-            canvas.toBlob(blob => {
-                if (blob) {
-                    resolve(blob);
-                }
-            }, 'image/png');
-        });
-
-    // given a square api image, get the cropped img
-    static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => {
-        // Create a new canvas element
-        const canvas = document.createElement('canvas');
-        canvas.width = width;
-        canvas.height = height;
-        const ctx = canvas.getContext('2d');
-        if (ctx) {
-            // Clear the canvas
-            ctx.clearRect(0, 0, canvas.width, canvas.height);
-            if (width < height) {
-                // horizontal padding, x offset
-                const xOffset = (canvasSize - width) / 2;
-                ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
-            } else {
-                // vertical padding, y offset
-                const yOffset = (canvasSize - height) / 2;
-                ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
-            }
-            return canvas;
-        }
-        return undefined;
-    };
-
-    // converts an image to a canvas data url
-    static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> =>
-        new Promise<string>((resolve, reject) => {
-            const img = new Image();
-            img.onload = () => {
-                const canvas = this.getCroppedImg(img, width, height);
-                if (canvas) {
-                    const dataUrl = canvas.toDataURL();
-                    resolve(dataUrl);
-                }
-            };
-            img.onerror = error => {
-                reject(error);
-            };
-            img.src = imageSrc;
-        });
-
-    // calls the openai api to get image edits
-    static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => {
-        const apiUrl = 'https://api.openai.com/v1/images/edits';
-        const fd = new FormData();
-        fd.append('image', imgBlob, 'image.png');
-        fd.append('mask', maskBlob, 'mask.png');
-        fd.append('prompt', prompt);
-        fd.append('size', '1024x1024');
-        fd.append('n', n ? JSON.stringify(n) : '1');
-        fd.append('response_format', 'b64_json');
-
-        try {
-            const res = await fetch(apiUrl, {
-                method: 'POST',
-                headers: {
-                    Authorization: `Bearer ${process.env.OPENAI_KEY}`,
-                },
-                body: fd,
-            });
-            const data = await res.json();
-            console.log(data.data);
-            return {
-                status: 'success',
-                urls: (data.data as { b64_json: string }[]).map(urlData => `data:image/png;base64,${urlData.b64_json}`),
-            };
-        } catch (err) {
-            console.log(err);
-            return { status: 'error', message: 'API error.' };
-        }
-    };
-
-    // mock api call
-    static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => ({
-        status: 'success',
-        urls: [mockSrc, mockSrc, mockSrc],
-    });
-
-    // Gets the canvas rendering context of a canvas
-    static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => {
-        if (!canvasRef.current) return null;
-        const ctx = canvasRef.current.getContext('2d');
-        if (!ctx) return null;
-        return ctx;
-    };
-
-    // Helper for downloading the canvas (for debugging)
-    static downloadCanvas = (canvas: HTMLCanvasElement) => {
-        const url = canvas.toDataURL();
-        const downloadLink = document.createElement('a');
-        downloadLink.href = url;
-        downloadLink.download = 'canvas';
-
-        downloadLink.click();
-        downloadLink.remove();
-    };
-
-    // Download the canvas (for debugging)
-    static downloadImageCanvas = (imgUrl: string) => {
-        const img = new Image();
-        img.src = imgUrl;
-        img.onload = () => {
-            const canvas = document.createElement('canvas');
-            canvas.width = canvasSize;
-            canvas.height = canvasSize;
-            const ctx = canvas.getContext('2d');
-            ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
-
-            this.downloadCanvas(canvas);
-        };
-    };
-
-    // Clears the canvas
-    static clearCanvas = (canvasRef: React.RefObject<HTMLCanvasElement>) => {
-        const ctx = this.getCanvasContext(canvasRef);
-        if (!ctx || !canvasRef.current) return;
-        ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
-    };
-
-    // Draws the image to the current canvas
-    static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => {
-        const drawImg = (htmlImg: HTMLImageElement) => {
-            const ctx = this.getCanvasContext(canvasRef);
-            if (!ctx) return;
-            ctx.globalCompositeOperation = 'source-over';
-            ctx.clearRect(0, 0, width, height);
-            ctx.drawImage(htmlImg, 0, 0, width, height);
-        };
-
-        if (img.complete) {
-            drawImg(img);
-        } else {
-            img.onload = () => {
-                drawImg(img);
-            };
-        }
-    };
-
-    // Gets the image mask for the openai endpoint
-    static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => {
-        const canvas = document.createElement('canvas');
-        canvas.width = canvasSize;
-        canvas.height = canvasSize;
-        const ctx = canvas.getContext('2d');
-        if (!ctx) return undefined;
-        ctx?.clearRect(0, 0, canvasSize, canvasSize);
-        ctx.drawImage(paddedCanvas, 0, 0);
-
-        // extract and set padding data
-        if (srcCanvas.height > srcCanvas.width) {
-            // horizontal padding, x offset
-            const xOffset = (canvasSize - srcCanvas.width) / 2;
-            ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height);
-            ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height);
-        } else {
-            // vertical padding, y offset
-            const yOffset = (canvasSize - srcCanvas.height) / 2;
-            ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height);
-            ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height);
-        }
-        return canvas;
-    };
-
-    // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas)
-    static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => {
-        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
-        const { data } = imageData;
-        for (let i = 0; i < canvas.height; i++) {
-            for (let j = 0; j < xOffset; j++) {
-                const targetIdx = 4 * (i * canvas.width + j);
-                const sourceI = i;
-                const sourceJ = xOffset + (xOffset - j);
-                const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
-                data[targetIdx] = data[sourceIdx];
-                data[targetIdx + 1] = data[sourceIdx + 1];
-                data[targetIdx + 2] = data[sourceIdx + 2];
-            }
-        }
-        for (let i = 0; i < canvas.height; i++) {
-            for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) {
-                const targetIdx = 4 * (i * canvas.width + j);
-                const sourceI = i;
-                const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j));
-                const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
-                data[targetIdx] = data[sourceIdx];
-                data[targetIdx + 1] = data[sourceIdx + 1];
-                data[targetIdx + 2] = data[sourceIdx + 2];
-            }
-        }
-        ctx.putImageData(imageData, 0, 0);
-    };
-
-    // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas)
-    static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => {
-        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
-        const { data } = imageData;
-        for (let j = 0; j < canvas.width; j++) {
-            for (let i = 0; i < yOffset; i++) {
-                const targetIdx = 4 * (i * canvas.width + j);
-                const sourceJ = j;
-                const sourceI = yOffset + (yOffset - i);
-                const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
-                data[targetIdx] = data[sourceIdx];
-                data[targetIdx + 1] = data[sourceIdx + 1];
-                data[targetIdx + 2] = data[sourceIdx + 2];
-            }
-        }
-        for (let j = 0; j < canvas.width; j++) {
-            for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) {
-                const targetIdx = 4 * (i * canvas.width + j);
-                const sourceJ = j;
-                const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i));
-                const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
-                data[targetIdx] = data[sourceIdx];
-                data[targetIdx + 1] = data[sourceIdx + 1];
-                data[targetIdx + 2] = data[sourceIdx + 2];
-            }
-        }
-        ctx.putImageData(imageData, 0, 0);
-    };
-
-    // Gets the unaltered (besides filling in padding) version of the image for the api call
-    static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => {
-        const canvas = document.createElement('canvas');
-        canvas.width = canvasSize;
-        canvas.height = canvasSize;
-        const ctx = canvas.getContext('2d');
-        if (!ctx) return undefined;
-        // fix scaling
-        const scale = Math.min(canvasSize / img.width, canvasSize / img.height);
-        const width = Math.floor(img.width * scale);
-        const height = Math.floor(img.height * scale);
-        ctx?.clearRect(0, 0, canvasSize, canvasSize);
-        ctx.fillStyle = bgColor;
-        ctx.fillRect(0, 0, canvasSize, canvasSize);
-
-        // extract and set padding data
-        if (img.naturalHeight > img.naturalWidth) {
-            // horizontal padding, x offset
-            const xOffset = Math.floor((canvasSize - width) / 2);
-            ctx.drawImage(img, xOffset, 0, width, height);
-
-            // draw reflected image padding
-            this.drawHorizontalReflection(ctx, canvas, xOffset);
-        } else {
-            // vertical padding, y offset
-            const yOffset = Math.floor((canvasSize - height) / 2);
-            ctx.drawImage(img, 0, yOffset, width, height);
-
-            // draw reflected image padding
-            this.drawVerticalReflection(ctx, canvas, yOffset);
-        }
-        return canvas;
-    };
-
-    /**
-     * Converts a url to base64 (tainted canvas workaround)
-     */
-    static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => {
-        try {
-            const res = await fetch(imageUrl);
-            const blob = await res.blob();
-
-            return new Promise<string>((resolve, reject) => {
-                const reader = new FileReader();
-                reader.onload = () => {
-                    const base64Data = reader.result?.toString().split(',')[1];
-                    if (base64Data) {
-                        resolve(base64Data);
-                    } else {
-                        reject(new Error('Failed to convert.'));
-                    }
-                };
-                reader.onerror = () => {
-                    reject(new Error('Error reading image data'));
-                };
-                reader.readAsDataURL(blob);
-            });
-        } catch (err) {
-            console.error(err);
-        }
-        return undefined;
-    };
-}
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 1a470f995..7a95e27c2 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,5 +1,6 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { ImageCast } from '../../../fields/Types';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
@@ -7,19 +8,32 @@ import { Docs } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants';
 
 export class DrawingFillHandler {
     static drawingToImage = (drawing: Doc, strength: number, prompt: string) =>
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
+                const aspectRatio = (drawing.width as number) / (drawing.height as number);
+                let dims: { width: number; height: number };
+                if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) {
+                    dims = FireflyDimensionsMap[FireflyImageDimensions.Widescreen];
+                } else if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Landscape]) {
+                    dims = FireflyDimensionsMap[FireflyImageDimensions.Landscape];
+                } else if (aspectRatio < AspectRatioLimits[FireflyImageDimensions.Portrait]) {
+                    dims = FireflyDimensionsMap[FireflyImageDimensions.Portrait];
+                } else {
+                    dims = FireflyDimensionsMap[FireflyImageDimensions.Square];
+                }
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
                 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
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl, strength }).then((info: Upload.ImageInformation) =>
+                            DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai_generated: true, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
+                        ); // prettier-ignore
                     });
             }
             return false;
diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts
new file mode 100644
index 000000000..f51305fba
--- /dev/null
+++ b/src/client/views/smartdraw/FireflyConstants.ts
@@ -0,0 +1,20 @@
+export enum FireflyImageDimensions {
+    Square = 'square',
+    Landscape = 'landscape',
+    Portrait = 'portrait',
+    Widescreen = 'widescreen',
+}
+
+export const FireflyDimensionsMap = {
+    square: { width: 2048, height: 2048 },
+    landscape: { width: 2304, height: 1792 },
+    portrait: { width: 1792, height: 2304 },
+    widescreen: { width: 2688, height: 1536 },
+};
+
+export const AspectRatioLimits = {
+    square: 1,
+    landscape: 1.167,
+    portrait: 0.875,
+    widescreen: 1.472,
+};
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
index 4b21c92a5..cca7d77c7 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.scss
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -1,6 +1,5 @@
 .smart-draw-handler {
     position: absolute;
-    width: 265px;
 
     .smart-draw-main {
         display: flex;
@@ -16,9 +15,11 @@
         display: flex;
         flex-direction: row;
         justify-content: center;
+        margin-top: 3px;
     }
 
     .smartdraw-options-container {
+        width: 265px;
         padding: 5px;
         font-weight: bolder;
         text-align: center;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 8cff2174f..6c9470480 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -25,6 +25,7 @@ import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkDash, ActiveInkFillCol
 import './SmartDrawHandler.scss';
 import { Networking } from '../../Network';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants';
 
 export interface DrawingOptions {
     text: string;
@@ -35,13 +36,6 @@ export interface DrawingOptions {
     y: number;
 }
 
-enum FireflyImageDimensions {
-    Square = 'square',
-    Landscape = 'landscape',
-    Portrait = 'portrait',
-    Widescreen = 'widescreen',
-}
-
 /**
  * The SmartDrawHandler allows users to generate drawings with GPT from text input. Users are able to enter
  * the item to draw, how complex they want the drawing to be, how large the drawing should be, and whether
@@ -274,30 +268,12 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     createImageWithFirefly = (input: string, seed?: number) => {
         this._lastInput.text = input;
-        let width = 0;
-        let height = 0;
-        switch (this._imgDims) {
-            case FireflyImageDimensions.Square:
-                width = 2048;
-                height = 2048;
-                break;
-            case FireflyImageDimensions.Landscape:
-                width = 2304;
-                height = 1792;
-            case FireflyImageDimensions.Portrait:
-                width = 1792;
-                height = 2304;
-                break;
-            case FireflyImageDimensions.Widescreen:
-                width = 2688;
-                height = 1536;
-                break;
-        }
-        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: width, height: height, seed: seed }).then(img => {
+        const dims = FireflyDimensionsMap[this._imgDims];
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed }).then(img => {
             const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                 title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
-                nativeWidth: width,
-                nativeHeight: height,
+                nativeWidth: dims.width,
+                nativeHeight: dims.height,
                 ai_generated: true,
                 firefly_seed: img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1],
                 firefly_prompt: input,
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index a41492745..4c4aac5e0 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,7 +20,7 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', structureUrl: string, strength: number) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50) =>
         this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
                 fetch('https://firefly-api.adobe.io/v3/images/generate', {
@@ -32,7 +32,8 @@ export default class FireflyManager extends ApiManager {
                         ['Authorization', `Bearer ${data.access_token}`],
                     ],
                     body: JSON.stringify({
-                        prompt,
+                        prompt: prompt,
+                        size: { width: width, height: height },
                         structure: !structureUrl
                             ? undefined
                             : {
@@ -226,7 +227,7 @@ export default class FireflyManager extends ApiManager {
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
                 this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
-                    this.generateImageFromStructure(req.body.prompt, uploadUrl, req.body.strength).then(fire =>
+                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength).then(fire =>
                         DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
                             if (info instanceof Error) _invalid(res, info.message);
                             else _success(res, info);
-- 
cgit v1.2.3-70-g09d2


From 6e7cb570f9bad527cd4772bb5c715dd588fb77df Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 2 Jan 2025 22:39:25 -0500
Subject: tags can now be used as style presets

---
 src/client/apis/gpt/GPT.ts                        |  2 +-
 src/client/views/DocumentButtonBar.tsx            | 18 ++++++++++++++++++
 src/client/views/nodes/DocumentView.tsx           |  2 ++
 src/client/views/smartdraw/DrawingFillHandler.tsx | 10 +++++++---
 src/client/views/smartdraw/FireflyConstants.ts    | 22 ++++++++++++++++++++++
 src/server/ApiManagers/FireflyManager.ts          |  7 +++++--
 6 files changed, 55 insertions(+), 6 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 9241eb120..4b0d58cc1 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -260,7 +260,7 @@ const gptDescribeImage = async (image: string): Promise<string> => {
                     content: [
                         {
                             type: 'text',
-                            text: `Identify what this drawing is, naming as many elements and their location in the drawing as possible`,
+                            text: `Briefly identify what this drawing is, naming all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
                         },
                         {
                             type: 'image_url',
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 32bf67df1..b7033af3f 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -314,6 +314,23 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
+    @computed
+    get aiEditorButton() {
+        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);
+                    }}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
+                </div>
+            </Tooltip>
+        );
+    }
+
     @observable _isRecording = false;
     _stopFunc: () => void = emptyFunction;
     @computed
@@ -484,6 +501,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 <div className="documentButtonBar-button">{this.calendarButton}</div>
+                <div className="documentButtonBar-button">{this.aiEditorButton}</div>
                 <div className="documentButtonBar-button">{this.keywordButton}</div>
                 {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 <div className="documentButtonBar-button">{this.menuButton}</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 8519cda3c..048b92c71 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1400,6 +1400,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     NativeHeight = () => this.effectiveNativeHeight;
     PanelWidth = () => this.panelWidth;
     PanelHeight = () => this.panelHeight;
+    ReducedPanelWidth = () => this.panelWidth / 2;
+    ReducedPanelHeight = () => this.panelWidth / 2;
     NativeDimScaling = () => this.nativeScaling;
     hideLinkCount = () => !!this.hideLinkButton;
     isHovering = () => this._isHovering;
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 7a95e27c2..52652d377 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -8,10 +8,13 @@ import { Docs } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
-import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions } from './FireflyConstants';
+import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
 export class DrawingFillHandler {
-    static drawingToImage = (drawing: Doc, strength: number, prompt: string) =>
+    static drawingToImage = (drawing: Doc, strength: number, prompt: string) => {
+        const docData = drawing[DocData];
+        const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
+        const styles = tags.filter(tag => FireflyStylePresets.has(tag));
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
@@ -31,11 +34,12 @@ export class DrawingFillHandler {
                 imageUrlToBase64(structureUrl)
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then((prompt: string) => {
-                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: prompt, width: dims.width, height: dims.height, structureUrl, strength }).then((info: Upload.ImageInformation) =>
+                        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)
                         ); // prettier-ignore
                     });
             }
             return false;
         });
+    };
 }
diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts
index f51305fba..3574039e4 100644
--- a/src/client/views/smartdraw/FireflyConstants.ts
+++ b/src/client/views/smartdraw/FireflyConstants.ts
@@ -18,3 +18,25 @@ export const AspectRatioLimits = {
     portrait: 0.875,
     widescreen: 1.472,
 };
+
+// prettier-ignore
+export const FireflyStylePresets = 
+    new Set<string>(['graphic', 'wireframe',
+        'vector_look','bw','cool_colors','golden','monochromatic','muted_color','toned_image','vibrant_colors','warm_tone','closeup',
+        'knolling','landscape_photography','macrophotography','photographed_through_window','shallow_depth_of_field','shot_from_above',
+        'shot_from_below','surface_detail','wide_angle','beautiful','bohemian','chaotic','dais','divine','eclectic','futuristic','kitschy',
+        'nostalgic','simple','antique_photo','bioluminescent','bokeh','color_explosion','dark','faded_image','fisheye','gomori_photography',
+        'grainy_film','iridescent','isometric','misty','neon','otherworldly_depiction','ultraviolet','underwater', 'backlighting', 
+        'dramatic_light', 'golden_hour', 'harsh_light','long','low_lighting','multiexposure','studio_light','surreal_lighting',
+        '3d_patterns','charcoal','claymation','fabric','fur','guilloche_patterns','layered_paper','marble_sculpture','made_of_metal',
+        'origami','paper_mache','polka','strange_patterns','wood_carving','yarn','art_deco','art_nouveau','baroque','bauhaus',
+        'constructivism','cubism','cyberpunk','fantasy','fauvism', 'film_noir','glitch_art','impressionism','industrialism','maximalism',
+        'minimalism','modern_art','modernism','neo','pointillism','psychedelic','science_fiction','steampunk','surrealism','synthetism',
+        'synthwave','vaporwave','acrylic_paint','bold_lines','chiaroscuro','color_shift_art','daguerreotype','digital_fractal',
+        'doodle_drawing','double_exposure_portrait','fresco','geometric_pen','halftone','ink','light_painting','line_drawing','linocut',
+        'oil_paint','paint_spattering','painting','palette_knife','photo_manipulation','scribble_texture','sketch','splattering',
+        'stippling_drawing','watercolor','3d','anime','cartoon','cinematic','comic_book','concept_art','cyber_matrix','digital_art',
+        'flat_design','geometric','glassmorphism','glitch_graphic','graffiti','hyper_realistic','interior_design','line_gradient',
+        'low_poly','newspaper_collage','optical_illusion','pattern_pixel','pixel_art','pop_art','product_photo','psychedelic_background',
+        'psychedelic_wonderland','scandinavian','splash_images','stamp','trompe_loeil'
+    ]);
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 4c4aac5e0..cb5d7c1da 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,9 +20,10 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[]) =>
         this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
+                //prettier-ignore
                 fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
@@ -42,6 +43,8 @@ export default class FireflyManager extends ApiManager {
                                       source: { url: structureUrl },
                                   },
                               },
+                        //prettier-ignore
+                        style: { presets: styles }
                     }),
                 })
                     .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
@@ -227,7 +230,7 @@ export default class FireflyManager extends ApiManager {
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
                 this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
-                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength).then(fire =>
+                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
                         DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
                             if (info instanceof Error) _invalid(res, info.message);
                             else _success(res, info);
-- 
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/DrawingFillHandler.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 f6a2df5bf15a3139e7fe35acc7fbf9d49b33d279 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 8 Jan 2025 15:03:49 -0500
Subject: get drawing prompt for firefly from title, then from gpt.  fix
 rendering freeform icons to ignore tags and other docview chrome.  fixed
 refStrength for creating images from properties view.

---
 src/client/views/PropertiesView.tsx                           |  6 ++----
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx |  2 +-
 src/client/views/smartdraw/DrawingFillHandler.tsx             | 11 +++++++----
 3 files changed, 10 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 54bcc02bc..e52189f56 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -998,7 +998,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 align="flex-start"
                                 fillWidth
                                 toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, 'fill in the details of this image'), 'createImage')}
+                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
                             />
                         </div>
                         <div className="strength-slider">{strength}</div>
@@ -1058,9 +1058,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @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);
-        });
+        this.selectedDoc[DocData].drawing_refStrength = Number(value);
     }
     @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore
     set smoothAmt(value) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 14b9ff4c3..20b91b6e4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1866,7 +1866,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     updateIcon = (usePanelDimensions?: boolean) => {
-        const contentDiv = this.DocumentView?.().ContentDiv;
+        const contentDiv = this._mainCont;
         return !contentDiv
             ? new Promise<void>(res => res())
             : UpdateIcon(
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 8e41ee105..fea4acb67 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -11,7 +11,7 @@ import { OpenWhere } from '../nodes/OpenWhere';
 import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
 export class DrawingFillHandler {
-    static drawingToImage = (drawing: Doc, strength: number, prompt: string) => {
+    static drawingToImage = (drawing: Doc, strength: number, user_prompt: string) => {
         const docData = drawing[DocData];
         const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
@@ -33,9 +33,12 @@ export class DrawingFillHandler {
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
                 imageUrlToBase64(structureUrl)
                     .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: 'firefly', ai_firefly_prompt: prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
+                    .then(prompt => {
+                        Networking.PostToServer('/queryFireflyImageFromStructure', 
+                            { prompt: user_prompt || prompt, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            .then((info: Upload.ImageInformation) =>
+                                DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
+                                    { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
                         ); // prettier-ignore
                     });
             }
-- 
cgit v1.2.3-70-g09d2


From dd0526a0a256fb3f1de9e0a2508566adcd6cd7e9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 9 Jan 2025 00:02:10 -0500
Subject: fixed sizing of images created by firefly.  added error message for
 failed firefly request.

---
 src/client/Network.ts                              |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  4 +-
 src/server/ApiManagers/FireflyManager.ts           | 47 +++++++++++++---------
 src/server/RouteManager.ts                         |  3 +-
 5 files changed, 35 insertions(+), 25 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index 9afdc844f..3b0406141 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -15,7 +15,7 @@ export namespace Networking {
         return (await fetch(relativeRoute)).text();
     }
 
-    export async function PostToServer(relativeRoute: string, body?: unknown) {
+    export function PostToServer(relativeRoute: string, body?: unknown) {
         const options = {
             uri: ClientUtils.prepend(relativeRoute),
             method: 'POST',
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ddc50871d..5524fedb3 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -586,7 +586,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     /**
      * When this is called, returns the list of documents that have been selected by the marquee box.
      */
-    marqueeSelect(selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) {
+    marqueeSelect = (selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) => {
         const selection: Doc[] = [];
         const selectFunc = (doc: Doc) => {
             const layoutDoc = Doc.Layout(doc);
@@ -619,7 +619,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                 .filter(doc => doc.z !== undefined)
                 .map(selectFunc);
         return selection;
-    }
+    };
 
     @computed get marqueeDiv() {
         const cpt = this._lassoFreehand || !this._visible ? [0, 0] : [this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY];
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index fea4acb67..96b21123b 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -38,8 +38,8 @@ export class DrawingFillHandler {
                             { prompt: user_prompt || prompt, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
-                                    { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
-                        ); // prettier-ignore
+                                    { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
+                        ).catch(e => alert("create image failed: " + e.toString())); // prettier-ignore
                     });
             }
             return false;
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 6daa5840e..a1f8fab8d 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -56,15 +56,22 @@ export default class FireflyManager extends ApiManager {
         );
 
     uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
-        new Promise<string>((res, rej) =>
+        new Promise<string | Error>((res, rej) =>
             fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
                 if (err) {
                     console.log('Error: ', err);
                     rej();
                 } else {
-                    dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }).then(response => {
-                        dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }).then(link => res(link.result.link));
-                    });
+                    dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents })
+                        .then(response => {
+                            dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' })
+                                .then(link => res(link.result.link))
+                                .catch(e => res(new Error(e.toString())));
+                        })
+                        .catch(e => {
+                            console.log('Dropbox error:', e);
+                            res(new Error(e.toString()));
+                        });
                 }
             })
         );
@@ -229,12 +236,14 @@ export default class FireflyManager extends ApiManager {
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
                 this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
-                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
-                        DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                            if (info instanceof Error) _invalid(res, info.message);
-                            else _success(res, info);
-                        })
-                    )
+                    uploadUrl instanceof Error
+                        ? _invalid(res, uploadUrl.message)
+                        : this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
+                              DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
+                                  if (info instanceof Error) _invalid(res, info.message);
+                                  else _success(res, info);
+                              })
+                          )
                 ),
         });
         register({
@@ -267,14 +276,16 @@ export default class FireflyManager extends ApiManager {
             subscription: '/expandImage',
             secureHandler: ({ req, res }) =>
                 this.uploadImageToDropbox(req.body.file).then(uploadUrl =>
-                    this.expandImage(uploadUrl, req.body.prompt).then(text => {
-                        if (text.error_code) _error(res, text.message);
-                        else
-                            DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
-                                if (info instanceof Error) _invalid(res, info.message);
-                                else _success(res, info);
-                            });
-                    })
+                    uploadUrl instanceof Error
+                        ? _invalid(res, uploadUrl.message)
+                        : this.expandImage(uploadUrl, req.body.prompt).then(text => {
+                              if (text.error_code) _error(res, text.message);
+                              else
+                                  DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => {
+                                      if (info instanceof Error) _invalid(res, info.message);
+                                      else _success(res, info);
+                                  });
+                          })
                 ),
         });
     }
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index d8e0455f6..2f6cf80b5 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -39,8 +39,7 @@ export function _success(res: Response, body: any) {
 }
 
 export function _invalid(res: Response, message: string) {
-    res.statusMessage = message;
-    res.status(STATUS.BAD_REQUEST).send();
+    res.status(STATUS.BAD_REQUEST).send(message);
 }
 
 export function _permissionDenied(res: Response, message?: string) {
-- 
cgit v1.2.3-70-g09d2


From 51a9f85b4ddc38ac825efcefc0c6db23f3b9914e Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 9 Jan 2025 11:19:31 -0500
Subject: added ai editor to collections

---
 src/client/apis/gpt/GPT.ts                         |  2 +-
 src/client/documents/Documents.ts                  |  1 +
 .../collectionFreeForm/CollectionFreeFormView.scss | 29 +++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 98 +++++++++++++++-------
 src/client/views/nodes/DocumentView.scss           |  2 +
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/ImageBox.scss               |  2 -
 src/client/views/nodes/ImageBox.tsx                | 73 +++++++++-------
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  2 +-
 10 files changed, 151 insertions(+), 64 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b0d58cc1..cf3a28a8e 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -260,7 +260,7 @@ const gptDescribeImage = async (image: string): Promise<string> => {
                     content: [
                         {
                             type: 'text',
-                            text: `Briefly identify what this drawing is, naming all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
+                            text: `Very briefly identify what this drawing is and list all the drawing elements and their location within the image. Do not include anything about the drawing style.`,
                         },
                         {
                             type: 'image_url',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 7f1387ff8..0bff74ac1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -19,6 +19,7 @@ import { DocServer } from '../DocServer';
 import { dropActionType } from '../util/DropActionTypes';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
 import { Id } from '../../fields/FieldSymbols';
+import { FireflyImageData } from '../views/smartdraw/FireflyConstants';
 
 class EmptyBox {
     public static LayoutString() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 2c94446fb..dff2cb282 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -304,3 +304,32 @@
         display: none;
     }
 }
+
+.collectionFreeformView-aiView {
+    text-align: center;
+    font-weight: bold;
+    width: 100%;
+
+    .collectionfreeformview-aiView-prompt {
+        height: 25px;
+    }
+
+    .collectionFreeFormView-aiView-strength {
+        text-align: center;
+    }
+
+    .collectionFreeformView-aiView-options-container,
+    .collectionFreeFormView-aiView-regenerate-container {
+        text-align: start;
+        font-weight: normal;
+        padding: 5px;
+    }
+    .collectionFreeformView-aiView-options,
+    .collectionFreeFormView-aiView-regenerate {
+        display: flex;
+        flex-direction: row;
+        gap: 10px;
+        justify-content: center;
+        align-items: center;
+    }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 7834eb352..112bfd178 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,9 +1,5 @@
 import { Bezier } from 'bezier-js';
-<<<<<<< HEAD
-import { Button, Colors, Type } from 'browndash-components';
-=======
-import { Colors } from '@dash/components';
->>>>>>> 2f7d1f0073943e1eb9e0f34c4459bc0176377697
+import { Button, Colors, Type } from '@dash/components';
 import { Property } from 'csstype';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -73,6 +69,9 @@ import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import ReactLoading from 'react-loading';
 import { SettingsManager } from '../../../util/SettingsManager';
+import { Slider } from '@mui/material';
+import { AiOutlineSend } from 'react-icons/ai';
+import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -2194,8 +2193,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @observable private _regenInput = '';
+    @observable private _drawingFillInput = '';
     @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
+    @observable private _regenLoading = false;
+    @observable private _drawingFillLoading = false;
+    @observable private _fireflyRefStrength = 50;
+    // _oldDrag: HTMLElement | null = null;
+
+    // onPassiveDrag = (e: PointerEvent) => e.stopPropagation();
+    // protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+    //     // this._dropDisposer?.();
+    //     this._oldDrag?.removeEventListener('pointerdown', this.onPassiveDrag);
+    //     this._oldDrag = ele;
+    //     // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+    //     ele?.addEventListener('pointerdown', this.onPassiveDrag, { passive: false });
+    // };
 
     componentAIViewHistory = () => {
         return (
@@ -2218,49 +2230,79 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     componentAIView = () => {
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="collectionfreeformview-aiView">
-                Edit Image with AI
+            <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
+                Edit Collection with AI
                 {showRegenerate && (
                     <div className="collectionfreeformview-aiView-regenerate-container">
-                        {/* <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
+                        <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
                         <div className="collectionfreeformview-aiView-regenerate">
                             <input
                                 className="collectionfreeformview-aiView-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}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newDrawings => {
-                                        if (newDrawings[0]) {
-                                        }
-                                    });
+                                    this._regenLoading = true;
+                                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                                    this._regenLoading = false;
                                 })}
                             />
-                            <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>
                 )}
                 <div className="collectionfreeformview-aiView-options-container">
-                    {showRegenerate && <text className="collectionfreeformview-aiView-subtitle"> Turn Drawing to Image </text>}
-                    <div className="collectionfreeformview-aiView-options"></div>
+                    <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
+                    <div className="collectionfreeformview-aiView-options">
+                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <div className="collectionfreeformview-aiView-strength">
+                            Reference Strength
+                            <Slider
+                                className="collectionfreeformview-aiView-slider"
+                                sx={{
+                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                }}
+                                min={1}
+                                max={100}
+                                step={1}
+                                size="small"
+                                value={this._fireflyRefStrength}
+                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                valueLabelDisplay="auto"
+                            />
+                        </div>
+                        <Button
+                            text="Send"
+                            type={Type.SEC}
+                            icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={undoable(
+                                action(() => {
+                                    this._drawingFillLoading = true;
+                                    DrawingFillHandler.drawingToImage(
+                                        this.props.Document,
+                                        this._fireflyRefStrength,
+                                        this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
+                                    );
+                                    this._drawingFillInput = '';
+                                    this._drawingFillLoading = false;
+                                }),
+                                'create image'
+                            )}
+                        />
+                    </div>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 82195b9c1..a3d47290a 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -281,6 +281,8 @@
 .documentView-editorView {
     width: 100%;
     overflow-y: scroll;
+    justify-items: center;
+    background-color: rgb(223, 223, 223);
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 622eccc4f..d656bcf50 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -716,8 +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;
+    rpw = () => this._props.PanelWidth() / 2;
+    rph = () => this.panelHeight() / 2;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index e083f52b4..9532f4ad3 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -159,8 +159,6 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
-    align-content: center;
-    height: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fe069eacc..db8bb2c6e 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, Button, Type } from '@dash/components';
+import { Colors, Button, Type, Size } from '@dash/components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { extname } from 'path';
@@ -76,6 +76,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     _ffref = React.createRef<CollectionFreeFormView>();
+    _oldWheel: HTMLElement | null = null;
     private _ignoreScroll = false;
     private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
@@ -94,6 +95,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _error = '';
     @observable private _isHovering = false; // flag to switch between primary and alternate images on hover
 
+    // variables for AI Image Editor
+    @observable private _regenInput = '';
+    @observable private _canInteract = true;
+    @observable private _regenerateLoading = false;
+    @observable private _prevImgs: FireflyImageData[] = [];
+
     constructor(props: FieldViewProps) {
         super(props);
         makeObservable(this);
@@ -529,25 +536,34 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
-    @observable private _regenInput = '';
-    @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = [];
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+
+    protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+        // this._dropDisposer?.();
+        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+        this._oldWheel = ele;
+        // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+        ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+    };
 
     componentAIViewHistory = () => {
         return (
-            <div className="imageBox-aiView-history">
+            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
+                <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
                 {this._prevImgs.map(img => (
-                    <img
-                        key={img.pathname}
-                        className="imageBox-aiView-img"
-                        src={img.href}
-                        onClick={() => {
-                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                            this.dataDoc.ai_firefly_prompt = img.prompt;
-                            this.dataDoc.ai_firefly_seed = img.seed;
-                        }}
-                    />
+                    <div>
+                        <img
+                            key={img.pathname}
+                            className="imageBox-aiView-img"
+                            src={img.href}
+                            onClick={() => {
+                                this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                                this.dataDoc.ai_firefly_prompt = img.prompt;
+                                this.dataDoc.ai_firefly_seed = img.seed;
+                            }}
+                        />
+                        <text>{img.prompt}</text>
+                    </div>
                 ))}
             </div>
         );
@@ -557,7 +573,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
         const showRegenerate = this.Document[DocData].ai;
         return (
-            <div className="imageBox-aiView">
+            <div
+                className="imageBox-aiView"
+                ref={(ele: HTMLDivElement | null) => {
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel = ele;
+                    // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
+                    ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                }}>
                 Edit Image with AI
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
@@ -566,11 +589,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             <input
                                 className="imageBox-aiView-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
@@ -587,22 +608,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                             const imgField = new ImageField(url);
                                             this._prevImgs.length === 0 &&
                                                 this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this.dataDoc[this.fieldKey] = imgField;
                                             this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
+                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
+                                            this.dataDoc[this.fieldKey] = imgField;
                                             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}
-                            />
+                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
                     </div>
                 )}
@@ -704,8 +719,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] });
         return { width, height };
     };
-
     savedAnnotations = () => this._savedAnnotations;
+
     render() {
         TraceMobx();
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index fea4acb67..43d16df89 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 => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: user_prompt || prompt, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
                                     { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, nativeWidth: dims.width, nativeHeight: dims.height }), OpenWhere.addRight)
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 9248cbee3..4052ea852 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -279,7 +279,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
                     ai: 'firefly',
-                    ai_firefly_seed: aiseed,
+                    ai_firefly_seed: seed,
                     ai_firefly_prompt: input,
                 });
                 DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
-- 
cgit v1.2.3-70-g09d2


From fd922d7898ed7a405ed47a7e48b85c582d787c07 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Fri, 10 Jan 2025 15:07:42 -0500
Subject: working on merge conflicts

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++--
 src/client/views/nodes/DocumentView.scss           |  2 +
 src/client/views/nodes/ImageBox.scss               |  8 +++-
 src/client/views/nodes/ImageBox.tsx                | 49 ++++++++++++++--------
 src/client/views/smartdraw/DrawingFillHandler.tsx  | 19 +++++++--
 src/server/ApiManagers/FireflyManager.ts           | 41 ++++++++++++------
 6 files changed, 89 insertions(+), 39 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 112bfd178..e1144f21a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2266,7 +2266,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     <div className="collectionfreeformview-aiView-options">
                         <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
                         <div className="collectionfreeformview-aiView-strength">
-                            Reference Strength
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
                                 sx={{
@@ -2282,6 +2281,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
                                 valueLabelDisplay="auto"
                             />
+                            Reference Strength
                         </div>
                         <Button
                             text="Send"
@@ -2295,9 +2295,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                         this.props.Document,
                                         this._fireflyRefStrength,
                                         this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
-                                    );
-                                    this._drawingFillInput = '';
-                                    this._drawingFillLoading = false;
+                                    ).then(() => {
+                                        this._drawingFillInput = '';
+                                        this._drawingFillLoading = false;
+                                    });
                                 }),
                                 'create image'
                             )}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index a3d47290a..e71f391c6 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -276,11 +276,13 @@
     right: 0;
     top: 0;
     overflow-y: scroll;
+    scrollbar-width: thin;
 }
 
 .documentView-editorView {
     width: 100%;
     overflow-y: scroll;
+    scrollbar-width: thin;
     justify-items: center;
     background-color: rgb(223, 223, 223);
 
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 1b6b3c85a..759f2584b 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -144,16 +144,20 @@
     display: flex;
     flex-direction: column;
     align-items: center;
-    padding: 5 0 0 5;
-    gap: 5px;
+    text-align: center;
 
     .imageBox-aiView-img {
         width: 100%;
+        padding: 5px;
 
         &:hover {
             filter: brightness(0.8);
         }
     }
+
+    .imageBox-aiView-caption {
+        font-size: 7px;
+    }
 }
 
 .imageBox-aiView {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9838af4d0..8a7fb99b1 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -536,23 +536,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
 
     componentAIViewHistory = () => {
+        const imgs: FireflyImageData[] = this.dataDoc.ai_firefly_history ? JSON.parse(StrCast(this.dataDoc.ai_firefly_history)) : [];
         return (
-            <div className="imageBox-aiView-history">
-                <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
-                {this._prevImgs.map(img => (
-                    <div key={img.pathname}>
-                        <img
-                            className="imageBox-aiView-img"
-                            src={img.href}
-                            onClick={() => {
-                                this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                                this.dataDoc.ai_firefly_prompt = img.prompt;
-                                this.dataDoc.ai_firefly_seed = img.seed;
-                            }}
-                        />
-                        <text>{img.prompt}</text>
-                    </div>
-                ))}
+            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
+                <Button
+                    text="Clear History"
+                    type={Type.SEC}
+                    style={{ marginTop: '5px' }}
+                    size={Size.XSMALL}
+                    onClick={action(() => {
+                        this._prevImgs = [];
+                        this.dataDoc.ai_firefly_history = undefined;
+                    })}
+                />
+                {imgs.length >= 2 &&
+                    imgs.map(img => (
+                        <div>
+                            <img
+                                key={img.pathname}
+                                className="imageBox-aiView-img"
+                                src={img.href}
+                                onClick={() => {
+                                    this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                                    this.dataDoc.ai_firefly_prompt = img.prompt;
+                                    this.dataDoc.ai_firefly_seed = img.seed;
+                                }}
+                            />
+                            <text className="imageBox-aiView-caption">{img.prompt.replace(/ ~~~/g, ',')}</text>
+                        </div>
+                    ))}
             </div>
         );
     };
@@ -562,6 +574,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
+                Edit Image with AI
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
                         <div className="imageBox-aiView-regenerate">
@@ -587,9 +600,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                             const imgField = new ImageField(url);
                                             this._prevImgs.length === 0 &&
                                                 this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
-                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
                                             this.dataDoc[this.fieldKey] = imgField;
+                                            this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
+                                            this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
                                             this._regenerateLoading = false;
                                             this._regenInput = '';
                                         }
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 423e3b2ac..57327631a 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,7 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { ImageCast } from '../../../fields/Types';
+import { DocCast, ImageCast } from '../../../fields/Types';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
@@ -11,10 +11,23 @@ import { OpenWhere } from '../nodes/OpenWhere';
 import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
 export class DrawingFillHandler {
-    static drawingToImage = (drawing: Doc, strength: number, user_prompt: string) => {
+    static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string) => {
         const docData = drawing[DocData];
         const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
+        const styleDocs = Doc.Links(drawing)
+            .map(link => Doc.getOppositeAnchor(link, drawing))
+            .map(anchor => anchor && DocCast(anchor.embedContainer));
+        const styleRef = styleDocs.filter(doc => doc !== undefined && doc.type === 'image').lastElement();
+        let styleUrl: string | undefined;
+        if (styleRef) {
+            const styleImg = await DocumentView.GetDocImage(styleRef);
+            if (styleImg) {
+                const { href } = ImageCast(styleImg).url;
+                const hrefParts = href.split('.');
+                styleUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
+            }
+        }
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
@@ -35,7 +48,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then(prompt => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength: strength, presets: styles, styleRef: styleUrl})
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
                                     { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index a1f8fab8d..185752404 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,7 +20,7 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[]) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string) =>
         this.getBearerToken().then(response =>
             response?.json().then((data: { access_token: string }) =>
                 //prettier-ignore
@@ -44,7 +44,12 @@ export default class FireflyManager extends ApiManager {
                                   },
                               },
                         //prettier-ignore
-                        style: { presets: styles }
+                        style: { 
+                            imageReference: {
+                                source: { uploadId: styleUrl }
+                            },
+                            presets: styles 
+                        }
                     }),
                 })
                     .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
@@ -235,16 +240,28 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
-                    uploadUrl instanceof Error
-                        ? _invalid(res, uploadUrl.message)
-                        : this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
-                              DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                                  if (info instanceof Error) _invalid(res, info.message);
-                                  else _success(res, info);
-                              })
-                          )
-                ),
+                this.uploadImageToDropbox(req.body.styleRef)
+                    .then(styleUrl => {
+                        if (styleUrl instanceof Error) {
+                            _invalid(res, styleUrl.message);
+                            throw new Error('Error uploading images to dropbox');
+                        }
+                        return this.uploadImageToDropbox(req.body.structure).then(structureUrl => {
+                            if (structureUrl instanceof Error) {
+                                _invalid(res, structureUrl.message);
+                                throw new Error('Error uploading images to dropbox');
+                            }
+                            return { styleUrl, structureUrl };
+                        });
+                    })
+                    .then(uploads =>
+                        this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl).then(fire =>
+                            DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
+                                if (info instanceof Error) _invalid(res, info.message);
+                                else _success(res, info);
+                            })
+                        )
+                    ),
         });
         register({
             method: Method.POST,
-- 
cgit v1.2.3-70-g09d2


From 12f5a435ee6476e2e07ded0c9cdd597c70ca8af0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 13 Jan 2025 22:26:35 -0500
Subject: changing ai for images and collections to work mostly the same way. 
 fixes for document view/collection/images to keep things working when the ai
 editor view reduces the rendered document size.  fixed so that freeform views
 overlaid on images/vieos/etc have the ui menu items of collections.

---
 src/ClientUtils.ts                                 |  23 +-
 src/client/util/SelectionManager.ts                |   3 +-
 src/client/views/GlobalKeyHandler.ts               |   8 +-
 src/client/views/PropertiesView.tsx                |  48 +++--
 src/client/views/TagsView.tsx                      |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  15 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 236 ++++++++++-----------
 src/client/views/nodes/DocumentView.scss           |   1 -
 src/client/views/nodes/DocumentView.tsx            |  48 +++--
 src/client/views/nodes/ImageBox.scss               |  19 +-
 src/client/views/nodes/ImageBox.tsx                | 113 ++++++----
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  17 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   8 +-
 src/server/ApiManagers/UploadManager.ts            |  22 +-
 14 files changed, 316 insertions(+), 247 deletions(-)

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

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 8f62b9060..e5dbb81db 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -144,15 +144,20 @@ export namespace ClientUtils {
     export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename: string | undefined = undefined) {
         try {
             const posting = ClientUtils.prepend('/uploadURI');
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename,
-                },
-                json: true,
-            });
+            const returnedUri = await rp
+                .post(posting, {
+                    body: {
+                        uri: imageUri,
+                        name: returnedFilename,
+                        nosuffix,
+                        replaceRootFilename,
+                    },
+                    json: true,
+                })
+                .catch(e => {
+                    alert('Data URI Error: ' + e.toString());
+                    return undefined;
+                });
             return returnedUri;
         } catch (e) {
             console.log('ConvertDataURI :' + e);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1ab84421c..a1f2849cd 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -88,7 +88,8 @@ ScriptingGlobals.add(function SelectedDocType(type: string, expertMode: boolean,
         return DocumentView.Selected().lastElement()?._props.renderDepth === 0;
     }
     const selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(DocumentView.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
-    return selected?.type === type || selected?.type_collection === type || !type;
+    const matchOverlayFreeform = type === CollectionViewType.Freeform && DocumentView.Selected().lastElement()?.ComponentView?.annotationKey;
+    return matchOverlayFreeform || selected?.type === type || selected?.type_collection === type || !type;
 });
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deselectAll() {
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f16338361..b200aff65 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -169,10 +169,10 @@ export class KeyManager {
                     return { stopPropagation: true, preventDefault: true };
                 }
                 break;
-            case 'arrowleft':  return this.nudge(-1,0, 'nudge left')
-            case 'arrowright': return this.nudge(1,0, 'nudge right');
-            case 'arrowup':    return this.nudge(0, -1, 'nudge up');
-            case 'arrowdown':  return this.nudge(0, 1, 'nudge down');
+            case 'arrowleft':  return (e.target as any).type !== 'text' && this.nudge(-1, 0, 'nudge left') // if target is an input box, then we don't want to nudge any Docs since we're justing moving within the text itself.
+            case 'arrowright': return (e.target as any).type !== 'text' && this.nudge( 1, 0, 'nudge right');
+            case 'arrowup':    return (e.target as any).type !== 'text' && this.nudge(0, -1, 'nudge up');
+            case 'arrowdown':  return (e.target as any).type !== 'text' && this.nudge(0,  1, 'nudge down');
             default:
         } // prettier-ignore
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e52189f56..42aa6782f 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -111,6 +111,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable openContexts: boolean = true;
     @observable openLinks: boolean = true;
     @observable openAppearance: boolean = true;
+    @observable openFirefly: boolean = true;
     @observable openTransform: boolean = true;
     @observable openFilters: boolean = false;
     @observable openStyling: boolean = true;
@@ -982,26 +983,10 @@ 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 && (
-                    <div>
-                        <div className="drawing-to-image">
-                            <Toggle
-                                text={'Create Image'}
-                                color={SettingsManager.userColor}
-                                icon={<FontAwesomeIcon icon="fill-drip" />}
-                                iconPlacement="left"
-                                align="flex-start"
-                                fillWidth
-                                toggleType={ToggleType.BUTTON}
-                                onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
-                            />
-                        </div>
-                        <div className="strength-slider">{strength}</div>
+                <div>
+                    {!targetDoc.layout_isSvg && this.containsInkDoc && (
                         <div className="color">
                             <Toggle
                                 text={'Color with GPT'}
@@ -1016,8 +1001,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                                 }, 'colorWithGPT')}
                             />
                         </div>
-                    </div>
-                )}
+                    )}
+                </div>
                 <div className="smooth">
                     <Toggle
                         text={'Smooth Ink Strokes'}
@@ -1113,6 +1098,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         this.openTransform = false;
         this.openFields = false;
         this.openSharing = false;
+        this.openAppearance = false;
+        this.openFirefly = false;
         this.openLayout = false;
         this.openFilters = false;
     };
@@ -1323,11 +1310,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get inkSubMenu() {
+        const strength = this.getNumber('Reference Strength', '', 1, 100, this.refStrength, (val: number) => {
+            !isNaN(val) && (this.refStrength = val);
+        });
+        const targetDoc = this.selectedLayoutDoc;
         return (
             <>
                 <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
                     {this.selectedStrokes.length ? this.inkEditor : null}
                 </PropertiesSection>
+                <PropertiesSection  title="Firefly" isOpen={this.openFirefly} setIsOpen={bool => { this.openFirefly = bool; }} onDoubleClick={this.CloseAll}>
+                    <>
+                <div className="drawing-to-image">
+                        <Toggle
+                            text="Create Image"
+                            color={SettingsManager.userColor}
+                            icon={<FontAwesomeIcon icon="fill-drip" />}
+                            iconPlacement="left"
+                            align="flex-start"
+                            fillWidth
+                            toggleType={ToggleType.BUTTON}
+                            onClick={undoable(() => DrawingFillHandler.drawingToImage(targetDoc, this.refStrength, StrCast(targetDoc.title) !== 'grouping' ? StrCast(targetDoc.title) : ''), 'createImage')}
+                        />
+                    </div>
+                    <div className="strength-slider">{strength}</div>
+                    </>
+                </PropertiesSection>
                 <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}>
                     {this.transformEditor}
                 </PropertiesSection>
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index 21b6a76c7..0edd89204 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -366,7 +366,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                     backgroundColor: this.isEditing ? Colors.LIGHT_GRAY : Colors.TRANSPARENT,
                     borderColor: this.isEditing ? Colors.BLACK : Colors.TRANSPARENT,
                     position: 'relative',
-                    top: this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
+                    top: this._props.Views.lastElement()._showAIEditor ? undefined : this._props.Views.length > 1 ? 25 : `calc(-${this.InsetDist} * ${1 / this.currentScale}px)`,
                 }}>
                 <div className="tagsView-content" style={{ width: '100%' }}>
                     <div className="tagsView-list">
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index dff2cb282..82887a7a5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -312,10 +312,14 @@
 
     .collectionfreeformview-aiView-prompt {
         height: 25px;
+        width: 100%;
     }
 
     .collectionFreeFormView-aiView-strength {
         text-align: center;
+        align-items: center;
+        display: flex;
+        width: 150px;
     }
 
     .collectionFreeformView-aiView-options-container,
@@ -323,13 +327,20 @@
         text-align: start;
         font-weight: normal;
         padding: 5px;
+        width: 100%;
+        display: flex;
+        .collectionFreeformView-aiView-subtitle {
+            margin: auto;
+            width: 40px;
+        }
     }
     .collectionFreeformView-aiView-options,
     .collectionFreeFormView-aiView-regenerate {
         display: flex;
         flex-direction: row;
-        gap: 10px;
-        justify-content: center;
         align-items: center;
+        align-items: center;
+        width: 100%;
+        gap: 10px;
     }
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 112bfd178..5b4f81379 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1877,8 +1877,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             : UpdateIcon(
                   this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
                   contentDiv,
-                  usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                  usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+                  usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+                  usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
                   this._props.PanelWidth(),
                   this._props.PanelHeight(),
                   0,
@@ -2228,45 +2228,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     componentAIView = () => {
-        const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}>
-                Edit Collection with AI
-                {showRegenerate && (
-                    <div className="collectionfreeformview-aiView-regenerate-container">
-                        <text className="collectionfreeformview-aiView-subtitle">Regenerate AI Image</text>
-                        <div className="collectionfreeformview-aiView-regenerate">
-                            <input
-                                className="collectionfreeformview-aiView-input"
-                                aria-label="Edit instructions input"
-                                type="text"
-                                value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                placeholder="Prompt (Optional)"
-                            />
-                            <Button
-                                text="Regenerate"
-                                type={Type.SEC}
-                                icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenLoading = true;
-                                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
-                                    SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-                                    SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                                    await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
-                                    this._regenLoading = false;
-                                })}
-                            />
-                        </div>
-                    </div>
-                )}
                 <div className="collectionfreeformview-aiView-options-container">
-                    <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
+                    <span className="collectionfreeformview-aiView-subtitle">Firefly:</span>
                     <div className="collectionfreeformview-aiView-options">
-                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <input
+                            className="collectionfreeformview-aiView-prompt"
+                            placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
+                            type="text"
+                            value={this._drawingFillInput}
+                            onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))}
+                        />
                         <div className="collectionfreeformview-aiView-strength">
-                            Reference Strength
+                            Similarity
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
                                 sx={{
@@ -2286,24 +2261,49 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         <Button
                             text="Send"
                             type={Type.SEC}
-                            icon={this._drawingFillLoading && this._drawingFillInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
                             onClick={undoable(
                                 action(() => {
                                     this._drawingFillLoading = true;
-                                    DrawingFillHandler.drawingToImage(
-                                        this.props.Document,
-                                        this._fireflyRefStrength,
-                                        this._drawingFillInput !== '' ? this._drawingFillInput : StrCast(this.props.Document.title) !== 'grouping' ? StrCast(this.props.Document.title) : ''
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
+                                        action(() => {
+                                            this._drawingFillLoading = false;
+                                        })
                                     );
-                                    this._drawingFillInput = '';
-                                    this._drawingFillLoading = false;
                                 }),
                                 'create image'
                             )}
                         />
                     </div>
                 </div>
+                <div className="collectionfreeformview-aiView-regenerate-container">
+                    <span className="collectionfreeformview-aiView-subtitle">Regenerate</span>
+                    <div className="collectionfreeformview-aiView-regenerate">
+                        <input
+                            className="collectionfreeformview-aiView-input"
+                            aria-label="Edit instructions input"
+                            type="text"
+                            value={this._regenInput}
+                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            placeholder="..under development.."
+                        />
+                        <Button
+                            text="Regenerate"
+                            type={Type.SEC}
+                            icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            // onClick={action(async () => {
+                            //     this._regenLoading = true;
+                            //     SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+                            //     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+                            //     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+                            //     await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true);
+                            //     this._regenLoading = false;
+                            // })}
+                        />
+                    </div>
+                </div>
             </div>
         );
     };
@@ -2311,83 +2311,81 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     render() {
         TraceMobx();
         return (
-            <div>
-                <div
-                    className="collectionfreeformview-container"
-                    id={this._paintedId}
-                    ref={r => {
-                        this.createDashEventsTarget(r);
-                        this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
-                        this._oldWheel = r;
-                        // prevent wheel events from passivly propagating up through containers
-                        r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
-                        r?.addEventListener('mouseleave', this.onMouseLeave);
-                        r?.addEventListener('mouseenter', this.onMouseEnter);
-                    }}
-                    onWheel={this.onPointerWheel}
-                    onClick={this.onClick}
-                    onPointerDown={this.onPointerDown}
-                    onPointerMove={this.onCursorMove}
-                    onDrop={this.onExternalDrop}
-                    onDragOver={e => e.preventDefault()}
-                    onContextMenu={this.onContextMenu}
-                    style={{
-                        pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(),
-                        textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
-                        transform: `scale(${this.nativeDimScaling})`,
-                        width: `${100 / this.nativeDimScaling}%`,
-                        height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
-                    }}>
-                    {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
-                        <div
-                            onPointerMove={this.onCursorMove}
-                            style={{
-                                position: 'fixed',
-                                left: this._eraserX,
-                                top: this._eraserY,
-                                width: (ActiveEraserWidth() + 5) * 2,
-                                height: (ActiveEraserWidth() + 5) * 2,
-                                borderRadius: '50%',
-                                border: '1px solid gray',
-                                transform: 'translate(-50%, -50%)',
-                            }}
+            <div
+                className="collectionfreeformview-container"
+                id={this._paintedId}
+                ref={r => {
+                    this.createDashEventsTarget(r);
+                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+                    this._oldWheel = r;
+                    // prevent wheel events from passivly propagating up through containers
+                    r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+                    r?.addEventListener('mouseleave', this.onMouseLeave);
+                    r?.addEventListener('mouseenter', this.onMouseEnter);
+                }}
+                onWheel={this.onPointerWheel}
+                onClick={this.onClick}
+                onPointerDown={this.onPointerDown}
+                onPointerMove={this.onCursorMove}
+                onDrop={this.onExternalDrop}
+                onDragOver={e => e.preventDefault()}
+                onContextMenu={this.onContextMenu}
+                style={{
+                    pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(),
+                    textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
+                    transform: `scale(${this.nativeDimScaling})`,
+                    width: `${100 / this.nativeDimScaling}%`,
+                    height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
+                }}>
+                {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
+                    <div
+                        onPointerMove={this.onCursorMove}
+                        style={{
+                            position: 'fixed',
+                            left: this._eraserX,
+                            top: this._eraserY,
+                            width: (ActiveEraserWidth() + 5) * 2,
+                            height: (ActiveEraserWidth() + 5) * 2,
+                            borderRadius: '50%',
+                            border: '1px solid gray',
+                            transform: 'translate(-50%, -50%)',
+                        }}
+                    />
+                )}
+                {this.paintFunc ? (
+                    <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
+                ) : this._lightboxDoc ? (
+                    <div style={{ padding: 15, width: '100%', height: '100%' }}>
+                        <DocumentView
+                            {...this._props}
+                            Document={this._lightboxDoc}
+                            containerViewPath={this.DocumentView?.().docViewPath}
+                            TemplateDataDocument={undefined}
+                            PanelWidth={this.lightboxPanelWidth}
+                            PanelHeight={this.lightboxPanelHeight}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
+                            onClickScript={this.onChildClickHandler}
+                            onKey={this.onKeyDown}
+                            onDoubleClickScript={this.onChildDoubleClickHandler}
+                            childFilters={this.childDocFilters}
+                            childFiltersByRanges={this.childDocRangeFilters}
+                            searchFilterDocs={this.searchFilterDocs}
+                            isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
+                            isContentActive={this._props.childContentsActive ?? emptyFunction}
+                            addDocTab={this.addDocTab}
+                            ScreenToLocalTransform={this.lightboxScreenToLocal}
+                            fitContentsToBox={undefined}
+                            focus={this.focus}
                         />
-                    )}
-                    {this.paintFunc ? (
-                        <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads
-                    ) : this._lightboxDoc ? (
-                        <div style={{ padding: 15, width: '100%', height: '100%' }}>
-                            <DocumentView
-                                {...this._props}
-                                Document={this._lightboxDoc}
-                                containerViewPath={this.DocumentView?.().docViewPath}
-                                TemplateDataDocument={undefined}
-                                PanelWidth={this.lightboxPanelWidth}
-                                PanelHeight={this.lightboxPanelHeight}
-                                NativeWidth={returnZero}
-                                NativeHeight={returnZero}
-                                onClickScript={this.onChildClickHandler}
-                                onKey={this.onKeyDown}
-                                onDoubleClickScript={this.onChildDoubleClickHandler}
-                                childFilters={this.childDocFilters}
-                                childFiltersByRanges={this.childDocRangeFilters}
-                                searchFilterDocs={this.searchFilterDocs}
-                                isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive}
-                                isContentActive={this._props.childContentsActive ?? emptyFunction}
-                                addDocTab={this.addDocTab}
-                                ScreenToLocalTransform={this.lightboxScreenToLocal}
-                                fitContentsToBox={undefined}
-                                focus={this.focus}
-                            />
-                        </div>
-                    ) : (
-                        <>
-                            {this._firstRender ? this.placeholder : this.marqueeView}
-                            {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
-                            {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
-                        </>
-                    )}
-                </div>
+                    </div>
+                ) : (
+                    <>
+                        {this._firstRender ? this.placeholder : this.marqueeView}
+                        {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
+                        {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
+                    </>
+                )}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index a3d47290a..8e7307d23 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -280,7 +280,6 @@
 
 .documentView-editorView {
     width: 100%;
-    overflow-y: scroll;
     justify-items: center;
     background-color: rgb(223, 223, 223);
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 640d27aa1..e37658ca5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -85,7 +85,7 @@ export interface DocumentViewProps extends FieldViewSharedProps {
     reactParent?: React.Component; // parent React component view (see CollectionFreeFormDocumentView)
 }
 @observer
-export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps>() {
+export class DocumentViewInternal extends DocComponent<FieldViewProps & DocumentViewProps & { showAIEditor: boolean }>() {
     // this makes mobx trace() statements more descriptive
     public get displayName()  { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
@@ -109,7 +109,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     private _mainCont = React.createRef<HTMLDivElement>();
     private _titleRef = React.createRef<EditableView>();
     private _dropDisposer?: DragManager.DragDropDisposer;
-    constructor(props: FieldViewProps & DocumentViewProps) {
+    constructor(props: FieldViewProps & DocumentViewProps & { showAIEditor: boolean }) {
         super(props);
         makeObservable(this);
     }
@@ -122,12 +122,6 @@ 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
@@ -689,7 +683,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
 
     rootSelected = () => this._rootSelected;
     panelHeight = () => this._props.PanelHeight() - this.headerMargin;
-    screenToLocalContent = () => this._props.ScreenToLocalTransform().translate(0, -this.headerMargin);
+    screenToLocalContent = () =>
+        this._props
+            .ScreenToLocalTransform()
+            .translate(0, -this.headerMargin)
+            .scale(this._props.showAIEditor ? (this._props.PanelHeight() || 1) / this.rph() : 1);
     onClickFunc = this.disableClickScriptFunc ? undefined : () => this.onClickHdlr;
     setHeight = (height: number) => { !this._props.suppressSetHeight && (this.layoutDoc._height = Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), height)); } // prettier-ignore
     setContentView = action((view: ViewBoxInterface<FieldViewProps>) => { this._componentView = view; }); // prettier-ignore
@@ -715,9 +713,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    @observable _aiWinHeight = 100;
-    rpw = () => this._props.PanelWidth() - this._aiWinHeight;
-    rph = () => this.panelHeight() - this._aiWinHeight;
+    @observable _aiWinHeight = 95;
+    rpw = () => (this.rph() * (this._props.NativeWidth?.() || 1)) / (this._props.NativeHeight?.() || 1);
+    rph = () => Math.max(10, this._props.PanelHeight() - this._aiWinHeight);
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -728,8 +726,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     className="documentView-contentsView"
                     style={{
                         pointerEvents: (isInk || noBackground ? 'none' : this.contentPointerEvents()) ?? (this._mounted ? 'all' : 'none'),
-                        width: this._showAIEditor ? this.rpw() : undefined,
-                        height: this._showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
+                        width: this._props.showAIEditor ? this.rpw() : undefined,
+                        height: this._props.showAIEditor ? this.rph() : this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
                     }}>
                     <DocumentContentsView
                         {...this._props}
@@ -737,8 +735,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
                         childFilters={this.childFilters}
-                        PanelHeight={this._showAIEditor ? this.rpw : this.panelHeight}
-                        PanelWidth={this._showAIEditor ? this.rph : this._props.PanelWidth}
+                        PanelWidth={this._props.showAIEditor ? this.rpw : this._props.PanelWidth}
+                        PanelHeight={this._props.showAIEditor ? this.rph : this.panelHeight}
                         setHeight={this.setHeight}
                         isContentActive={this.isContentActive}
                         ScreenToLocalTransform={this.screenToLocalContent}
@@ -748,7 +746,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                     />
                 </div>
-                {!this._showAIEditor ? null : (
+                {!this._props.showAIEditor ? null : (
                     <>
                         <div
                             className="documentView-editorView-history"
@@ -1168,10 +1166,13 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     @computed private get nativeScaling() {
         if (this.shouldNotScale) return 1;
         const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
-        if (this.layout_fitWidth || this._props.PanelHeight() / (this.effectiveNativeHeight || 1) > this._props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
-            return Math.max(minTextScale, this._props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth
+        const ai = this._showAIEditor && this.nativeWidth === this.layoutDoc.width ? 95 : 0;
+        const effNW = Math.max(this.effectiveNativeWidth - ai, 1);
+        const effNH = Math.max(this.effectiveNativeHeight - ai, 1);
+        if (this.layout_fitWidth || (this._props.PanelHeight() - ai) / effNH > (this._props.PanelWidth() - ai) / effNW) {
+            return Math.max(minTextScale, (this._props.PanelWidth() - ai) / effNW); // width-limited or layout_fitWidth
         }
-        return Math.max(minTextScale, this._props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
+        return Math.max(minTextScale, (this._props.PanelHeight() - ai) / effNH); // height-limited or unscaled
     }
     @computed private get panelWidth() {
         return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this._props.PanelWidth();
@@ -1231,7 +1232,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     }
 
     @computed get layout_fitWidth() {
-        return this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth;
+        return this._showAIEditor ? false : (this._props.fitWidth?.(this.layoutDoc) ?? this.layoutDoc?.layout_fitWidth);
     }
     @computed get anchorViewDoc() {
         return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document.link_anchor_2) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document.link_anchor_1) : undefined;
@@ -1327,9 +1328,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         }
     };
 
+    @observable public _showAIEditor: boolean = false;
+
     @action
     public toggleAIEditor = () => {
-        this._docViewInternal && this._docViewInternal.showAIEditor();
+        this._showAIEditor = !this._showAIEditor;
     };
 
     public setTextHtmlOverlay = action((text: string | undefined, effect?: Doc) => {
@@ -1516,6 +1519,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                         }}>
                         <DocumentViewInternal
                             {...this._props}
+                            showAIEditor={this._showAIEditor}
                             reactParent={undefined}
                             isHovering={this.isHovering}
                             fieldKey={this.LayoutFieldKey}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 1b6b3c85a..a50320f5e 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,6 +40,7 @@
     max-height: 100%;
     pointer-events: inherit;
     background: transparent;
+    z-index: -10000;
 
     img {
         height: auto;
@@ -159,6 +160,7 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
@@ -168,20 +170,29 @@
     .imageBox-aiView-regenerate-container,
     .imageBox-aiView-options-container {
         font-weight: normal;
-        margin: 5px;
         display: flex;
     }
 
     .imageBox-aiView-regenerate,
     .imageBox-aiView-options {
         display: flex;
-        flex-direction: row;
-        justify-content: center;
+        align-items: center;
         flex-direction: row;
         gap: 5px;
+        width: 100%;
     }
 
+    .imageBox-aiView-strength {
+        text-align: center;
+        align-items: center;
+        display: flex;
+        width: 125px;
+    }
+    .imageBox-aiView-slider {
+        width: 50px;
+        margin-left: 5px;
+    }
     .imageBox-aiView-input {
-        width: 50%;
+        width: 100%;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9838af4d0..9223fc180 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@mui/material';
+import { Slider, Tooltip } from '@mui/material';
 import axios from 'axios';
 import { Colors, Button, Type, Size } from '@dash/components';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction } from 'mobx';
@@ -44,6 +44,7 @@ import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { SettingsManager } from '../../util/SettingsManager';
 import { AiOutlineSend } from 'react-icons/ai';
 import { FireflyImageData } from '../smartdraw/FireflyConstants';
+import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -98,7 +99,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = [];
+    @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -386,8 +387,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             : UpdateIcon(
                   this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
                   contentDiv,
-                  usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
-                  usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
+                  usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+                  usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
                   this._props.PanelWidth(),
                   this._props.PanelHeight(),
                   0,
@@ -543,65 +544,91 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <div key={img.pathname}>
                         <img
                             className="imageBox-aiView-img"
-                            src={img.href}
+                            src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
                             onClick={() => {
                                 this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
                                 this.dataDoc.ai_firefly_prompt = img.prompt;
                                 this.dataDoc.ai_firefly_seed = img.seed;
                             }}
                         />
-                        <text>{img.prompt}</text>
+                        <span>{img.prompt}</span>
                     </div>
                 ))}
             </div>
         );
     };
+    @observable private _fireflyRefStrength = 0;
 
     componentAIView = () => {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
-        const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
-                {showRegenerate && (
-                    <div className="imageBox-aiView-regenerate-container">
-                        <div className="imageBox-aiView-regenerate">
-                            <input
-                                className="imageBox-aiView-input"
-                                aria-label="Edit instructions input"
-                                type="text"
-                                value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
-                                placeholder="Prompt (Optional)"
-                            />
-                            <Button
-                                text="Regenerate Image"
-                                type={Type.SEC}
-                                // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
-                                onClick={action(async () => {
-                                    this._regenerateLoading = true;
-                                    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);
-                                            this._prevImgs.length === 0 &&
-                                                this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                            this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
-                                            this.dataDoc.ai_firefly_history = `${this._prevImgs}`;
-                                            this.dataDoc[this.fieldKey] = imgField;
-                                            this._regenerateLoading = false;
-                                            this._regenInput = '';
-                                        }
-                                    });
-                                })}
+                <div className="imageBox-aiView-regenerate-container">
+                    <div className="imageBox-aiView-regenerate">
+                        Firefly:
+                        <input
+                            className="imageBox-aiView-input"
+                            aria-label="Edit instructions input"
+                            type="text"
+                            value={this._regenInput}
+                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            placeholder={this._regenInput || StrCast(this.Document.title)}
+                        />
+                        <div className="imageBox-aiView-strength">
+                            <span style={{ width: 60 }}>Similarity</span>
+                            <Slider
+                                className="imageBox-aiView-slider"
+                                sx={{
+                                    '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                                    '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
+                                    '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                                }}
+                                min={0}
+                                max={100}
+                                step={1}
+                                size="small"
+                                value={this._fireflyRefStrength}
+                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                valueLabelDisplay="auto"
                             />
-                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
+                        <Button
+                            text="Create"
+                            type={Type.SEC}
+                            // style={{ alignSelf: 'flex-end' }}
+                            icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                            iconPlacement="right"
+                            onClick={action(async () => {
+                                this._regenerateLoading = true;
+                                if (this._fireflyRefStrength) {
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(
+                                        action(() => {
+                                            this._regenerateLoading = false;
+                                        })
+                                    );
+                                } else
+                                    SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
+                                        action(newImgs => {
+                                            if (newImgs[0]) {
+                                                const url = newImgs[0].pathname;
+                                                const imgField = new ImageField(url);
+                                                this._prevImgs.length === 0 &&
+                                                    this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
+                                                this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url });
+                                                this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
+                                                this.dataDoc.ai_firefly_prompt = newImgs[0].prompt;
+                                                this.dataDoc[this.fieldKey] = imgField;
+                                                this._regenerateLoading = false;
+                                                this._regenInput = '';
+                                            }
+                                        })
+                                    );
+                            })}
+                        />
                     </div>
-                )}
+                </div>
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
+                    <span className="imageBox-aiView-subtitle"> More Options: </span>
                     <div className="imageBox-aiView-options">
                         <Button
                             type={Type.TERT}
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 423e3b2ac..d9a01c436 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -15,7 +15,7 @@ export class DrawingFillHandler {
         const docData = drawing[DocData];
         const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
-        DocumentView.GetDocImage(drawing)?.then(imageField => {
+        return DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
                 let dims: { width: number; height: number };
@@ -31,18 +31,17 @@ export class DrawingFillHandler {
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
-                imageUrlToBase64(structureUrl)
-                    .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
-                    .then(prompt => {
+                return imageUrlToBase64(structureUrl)
+                    .then(gptDescribeImage)
+                    .then(prompt => 
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                            { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
-                                    { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
-                        ).catch(e => alert("create image failed: " + e.toString())); // prettier-ignore
-                    });
+                                    { ai: 'firefly', title: user_prompt || prompt, ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
+                        ).catch(e => alert("create image failed: " + e.toString())) 
+                    ); // prettier-ignore
             }
-            return false;
         });
     };
 }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index f635b5642..7db9ef133 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -271,16 +271,16 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     createImageWithFirefly = (input: string, seed?: number, changeInPlace?: boolean): Promise<FireflyImageData> => {
         this._lastInput.text = input;
         const dims = FireflyDimensionsMap[this._imgDims];
-        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed: seed })
+        return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed })
             .then(img => {
-                const seed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+                const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
                 if (!changeInPlace) {
                     const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                         title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
                         nativeWidth: dims.width,
                         nativeHeight: dims.height,
                         ai: 'firefly',
-                        ai_firefly_seed: seed,
+                        ai_firefly_seed: newseed,
                         ai_firefly_prompt: input,
                     });
                     DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
@@ -306,7 +306,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     case DocumentType.IMG:
                         if (this._regenInput) {
                             // if (this._selectedDoc) {
-                            const newPrompt = `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}`;
+                            const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
                             return this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed), changeInPlace);
                             // }
                         }
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 5a880901b..c9d5df547 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -274,14 +274,20 @@ export default class UploadManager extends ApiManager {
                         .filter(f => regex.test(f))
                         .map(f => fs.unlinkSync(serverPath + f));
                 }
-                imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix))).then((savedName: string) => {
-                    const ext = path.extname(savedName).toLowerCase();
-                    const outputPath = serverPathToFile(Directory.images, filename + ext);
-                    if (AcceptableMedia.imageFormats.includes(ext)) {
-                        workerResample(savedName, outputPath, origSuffix, false);
-                    }
-                    res.send(clientPathToFile(Directory.images, filename + ext));
-                });
+                imageDataUri
+                    .outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, origSuffix)))
+                    .then((savedName: string) => {
+                        const ext = path.extname(savedName).toLowerCase();
+                        const outputPath = serverPathToFile(Directory.images, filename + ext);
+                        if (AcceptableMedia.imageFormats.includes(ext)) {
+                            workerResample(savedName, outputPath, origSuffix, false);
+                        }
+                        res.send(clientPathToFile(Directory.images, filename + ext));
+                    })
+                    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+                    .catch((e: any) => {
+                        res.status(404).json({ error: e.toString() });
+                    });
             },
         });
     }
-- 
cgit v1.2.3-70-g09d2


From 859bb1b733901e4697f1d53781af725e4830c607 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 14 Jan 2025 17:19:16 +0100
Subject: added drawing fill style reference

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 35 ++----------
 src/client/views/nodes/DocumentView.tsx            |  1 +
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/ImageBox.scss               |  9 +---
 src/client/views/nodes/ImageBox.tsx                | 62 +++++++++++++---------
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  2 +-
 src/server/ApiManagers/FireflyManager.ts           | 35 ++++++------
 7 files changed, 65 insertions(+), 80 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e1144f21a..66e0ba5d9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2194,38 +2194,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @observable private _regenInput = '';
     @observable private _drawingFillInput = '';
-    @observable private _canInteract = true;
     @observable private _regenLoading = false;
     @observable private _drawingFillLoading = false;
     @observable private _fireflyRefStrength = 50;
-    // _oldDrag: HTMLElement | null = null;
-
-    // onPassiveDrag = (e: PointerEvent) => e.stopPropagation();
-    // protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
-    //     // this._dropDisposer?.();
-    //     this._oldDrag?.removeEventListener('pointerdown', this.onPassiveDrag);
-    //     this._oldDrag = ele;
-    //     // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling
-    //     ele?.addEventListener('pointerdown', this.onPassiveDrag, { passive: false });
-    // };
-
-    componentAIViewHistory = () => {
-        return (
-            <div className="imageBox-aiView-history">
-                {/* {this._prevImgs.map(img => (
-                    <img
-                        className="imageBox-aiView-img"
-                        src={img.href}
-                        onClick={() => {
-                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                            this.dataDoc.ai_firefly_prompt = img.prompt;
-                            this.dataDoc.ai_firefly_seed = img.seed;
-                        }}
-                    />
-                ))} */}
-            </div>
-        );
-    };
 
     componentAIView = () => {
         const showRegenerate = this.Document[DocData].ai;
@@ -2241,7 +2212,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 aria-label="Edit instructions input"
                                 type="text"
                                 value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                onChange={action(e => (this._regenInput = e.target.value))}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
@@ -2264,7 +2235,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 <div className="collectionfreeformview-aiView-options-container">
                     <text className="collectionfreeformview-aiView-subtitle"> Create Image with Firefly </text>
                     <div className="collectionfreeformview-aiView-options">
-                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))} />
+                        <input className="collectionfreeformview-aiView-prompt" placeholder="Prompt (Optional)" type="text" value={this._drawingFillInput} onChange={action(e => (this._drawingFillInput = e.target.value))} />
                         <div className="collectionfreeformview-aiView-strength">
                             <Slider
                                 className="collectionfreeformview-aiView-slider"
@@ -2278,7 +2249,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 step={1}
                                 size="small"
                                 value={this._fireflyRefStrength}
-                                onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
+                                onChange={action((e, val) => (this._fireflyRefStrength = val as number))}
                                 valueLabelDisplay="auto"
                             />
                             Reference Strength
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 640d27aa1..44b214644 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -718,6 +718,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     @observable _aiWinHeight = 100;
     rpw = () => this._props.PanelWidth() - this._aiWinHeight;
     rph = () => this.panelHeight() - this._aiWinHeight;
+
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 02d4d9adb..2e40f39ed 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -68,6 +68,7 @@ export interface FieldViewSharedProps {
     isGroupActive?: () => string | undefined; // is this document part of a group that is active
     // eslint-disable-next-line no-use-before-define
     setContentViewBox?: (view: ViewBoxInterface<FieldViewProps>) => void; // called by rendered field's viewBox so that DocumentView can make direct calls to the viewBox
+
     PanelWidth: () => number;
     PanelHeight: () => number;
     isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 759f2584b..94a497cf0 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -163,6 +163,7 @@
 .imageBox-aiView {
     text-align: center;
     font-weight: bold;
+    width: 100%;
 
     .imageBox-aiView-subtitle {
         position: relative;
@@ -179,13 +180,7 @@
     .imageBox-aiView-regenerate,
     .imageBox-aiView-options {
         display: flex;
-        flex-direction: row;
-        justify-content: center;
-        flex-direction: row;
+        margin: auto;
         gap: 5px;
     }
-
-    .imageBox-aiView-input {
-        width: 50%;
-    }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 8a7fb99b1..3a9086777 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -96,8 +96,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
 
     // variables for AI Image Editor
     @observable private _regenInput = '';
-    @observable private _canInteract = true;
-    @observable private _regenerateLoading = false;
+    @observable private _regenLoading = false;
     @observable private _prevImgs: FireflyImageData[] = [];
 
     constructor(props: FieldViewProps) {
@@ -535,20 +534,31 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         );
     }
 
+    protected _btnWidth = 50;
+    protected _inputWidth = 50;
+    protected _sideBtnMaxPanelPct = 0.12;
+    @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+    @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
+    @computed get maxWidgetSize() { return Math.min(this._btnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width) ? 1 : this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+    @computed get uiBtnScaling() { return this.maxWidgetSize / this._btnWidth; } // prettier-ignore
+    @computed get uiInputScaling() { return this.maxWidgetSize / this._inputWidth; } // prettier-ignore
+
     componentAIViewHistory = () => {
         const imgs: FireflyImageData[] = this.dataDoc.ai_firefly_history ? JSON.parse(StrCast(this.dataDoc.ai_firefly_history)) : [];
         return (
-            <div className="imageBox-aiView-history" ref={this.createDashEventsTarget}>
-                <Button
-                    text="Clear History"
-                    type={Type.SEC}
-                    style={{ marginTop: '5px' }}
-                    size={Size.XSMALL}
-                    onClick={action(() => {
-                        this._prevImgs = [];
-                        this.dataDoc.ai_firefly_history = undefined;
-                    })}
-                />
+            <div className="imageBox-aiView-history">
+                {imgs.length >= 2 && (
+                    <Button
+                        text="Clear History"
+                        type={Type.SEC}
+                        style={{ marginTop: '5px' }}
+                        size={Size.XSMALL}
+                        onClick={action(() => {
+                            this._prevImgs = [];
+                            this.dataDoc.ai_firefly_history = undefined;
+                        })}
+                    />
+                )}
                 {imgs.length >= 2 &&
                     imgs.map(img => (
                         <div>
@@ -574,26 +584,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const showRegenerate = this.Document[DocData].ai;
         return (
             <div className="imageBox-aiView">
-                Edit Image with AI
+                <text>Edit Image with AI </text>
                 {showRegenerate && (
                     <div className="imageBox-aiView-regenerate-container">
                         <div className="imageBox-aiView-regenerate">
                             <input
+                                style={{ transform: `scale(${this.uiInputScaling})` }}
                                 className="imageBox-aiView-input"
                                 aria-label="Edit instructions input"
                                 type="text"
                                 value={this._regenInput}
-                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                onChange={action(e => (this._regenInput = e.target.value))}
                                 placeholder="Prompt (Optional)"
                             />
                             <Button
+                                style={{ transform: `scale(${this.uiBtnScaling})` }}
                                 text="Regenerate Image"
                                 type={Type.SEC}
-                                // style={{ alignSelf: 'flex-end' }}
-                                icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                iconPlacement="right"
                                 onClick={action(async () => {
-                                    this._regenerateLoading = true;
+                                    this._regenLoading = true;
                                     SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true).then(newImgs => {
                                         if (newImgs[0]) {
                                             const url = newImgs[0].pathname;
@@ -603,20 +612,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                             this.dataDoc[this.fieldKey] = imgField;
                                             this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, href: this.paths.lastElement(), pathname: url });
                                             this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
-                                            this._regenerateLoading = false;
+                                            this._regenLoading = false;
                                             this._regenInput = '';
                                         }
                                     });
                                 })}
                             />
-                            <Button text="Get Variations" type={Type.SEC} iconPlacement="right" />
+                            <Button style={{ transform: `scale(${this.uiBtnScaling})` }} text="Get Variations" type={Type.SEC} iconPlacement="right" />
                         </div>
                     </div>
                 )}
                 <div className="imageBox-aiView-options-container">
-                    {showRegenerate && <text className="imageBox-aiView-subtitle"> More Options: </text>}
                     <div className="imageBox-aiView-options">
+                        {showRegenerate && (
+                            <text className="imageBox-aiView-subtitle" style={{ transform: `scale(${this.uiBtnScaling})` }}>
+                                More Options:
+                            </text>
+                        )}
                         <Button
+                            style={{ transform: `scale(${this.uiBtnScaling})` }}
                             type={Type.TERT}
                             text="Get Text"
                             icon={<FontAwesomeIcon icon="font" />}
@@ -632,11 +646,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             }}
                         />
                         <Button
+                            style={{ transform: `scale(${this.uiBtnScaling})` }}
                             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;
@@ -646,11 +660,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             })}
                         />
                         <Button
+                            style={{ transform: `scale(${this.uiBtnScaling})` }}
                             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', {
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 57327631a..57e75e86a 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -48,7 +48,7 @@ export class DrawingFillHandler {
                     .then((hrefBase64: string) => gptDescribeImage(hrefBase64))
                     .then(prompt => {
                         Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength: strength, presets: styles, styleRef: styleUrl})
+                            { prompt: `${user_prompt}, ${prompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength: strength, presets: styles, styleUrl: styleUrl})
                             .then((info: Upload.ImageInformation) =>
                                 DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
                                     { ai: 'firefly', ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 185752404..d92db1522 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -20,11 +20,11 @@ export default class FireflyManager extends ApiManager {
             return undefined;
         });
 
-    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string) =>
+    generateImageFromStructure = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, structureUrl: string, strength: number = 50, styles: string[], styleUrl: string | undefined) =>
         this.getBearerToken().then(response =>
-            response?.json().then((data: { access_token: string }) =>
+            response?.json().then((data: { access_token: string }) => {
                 //prettier-ignore
-                fetch('https://firefly-api.adobe.io/v3/images/generate', {
+                return fetch('https://firefly-api.adobe.io/v3/images/generate', {
                     method: 'POST',
                     headers: [
                         ['Content-Type', 'application/json'],
@@ -43,12 +43,14 @@ export default class FireflyManager extends ApiManager {
                                       source: { url: structureUrl },
                                   },
                               },
-                        //prettier-ignore
-                        style: { 
-                            imageReference: {
-                                source: { uploadId: styleUrl }
-                            },
-                            presets: styles 
+                        // prettier-ignore
+                        style: {
+                            presets: styles,
+                            ...(styleUrl && {
+                                imageReference: {
+                                    source: { uploadId: styleUrl },
+                                },
+                            }),
                         }
                     }),
                 })
@@ -57,7 +59,7 @@ export default class FireflyManager extends ApiManager {
                         console.error('Error:', error);
                         return '';
                     })
-            )
+            })
         );
 
     uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
@@ -239,8 +241,8 @@ export default class FireflyManager extends ApiManager {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImageFromStructure',
-            secureHandler: async ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.styleRef)
+            secureHandler: async ({ req, res }) => {
+                (req.body.styleRef ? this.uploadImageToDropbox(req.body.styleRef) : Promise.resolve(undefined))
                     .then(styleUrl => {
                         if (styleUrl instanceof Error) {
                             _invalid(res, styleUrl.message);
@@ -255,13 +257,14 @@ export default class FireflyManager extends ApiManager {
                         });
                     })
                     .then(uploads =>
-                        this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl).then(fire =>
+                        this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, req.body.styleUrl).then(fire => {
                             DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
                                 if (info instanceof Error) _invalid(res, info.message);
                                 else _success(res, info);
-                            })
-                        )
-                    ),
+                            });
+                        })
+                    );
+            },
         });
         register({
             method: Method.POST,
-- 
cgit v1.2.3-70-g09d2


From 205fdacf9807be2691e13e1d9d04d6b98620391a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 15 Jan 2025 13:03:59 -0500
Subject: fixed so that users are sentt to dropbox to authorized uploads once,
 and then a refresh token is used afterwards.  fixed up some collection views
 to work better with the tagsView.  fixed grid view to show entire image. 
 tweaked notetaking view to work better with tags, but it's incomplete.

---
 .../views/collections/CollectionCarousel3DView.tsx |  1 +
 .../views/collections/CollectionNoteTakingView.tsx | 11 +--
 .../views/collections/CollectionStackingView.tsx   | 13 ++-
 .../collectionGrid/CollectionGridView.tsx          | 12 +--
 src/client/views/smartdraw/DrawingFillHandler.tsx  | 28 +++++--
 src/server/ApiManagers/FireflyManager.ts           | 94 ++++++++++++++++++++--
 src/server/authentication/DashUserModel.ts         |  5 ++
 7 files changed, 141 insertions(+), 23 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index b7ecf9a2f..9c8ef5519 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -136,6 +136,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
                 PanelWidth={this.panelWidth}
                 PanelHeight={this.panelHeight}
+                showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
             />
         );
 
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index ac8e37358..323a679cc 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils';
-import { Doc, Field, FieldType, Opt } from '../../../fields/Doc';
+import { Doc, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Copy, Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -48,6 +48,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     @computed get notetakingCategoryField() {
         return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column'));
     }
+    toHeader = (d: Doc) => (d[this.notetakingCategoryField] instanceof List ? StrListCast(d[this.notetakingCategoryField]).join('.') : (d[this.notetakingCategoryField] ?? 'unset'));
     public DividerWidth = 16;
     @observable docsDraggedRowCol: number[] = [];
     @observable _scroll = 0;
@@ -136,7 +137,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const rowCol = this.docsDraggedRowCol;
         // this will sort the docs into the correct columns (minus the ones you're currently dragging)
         docs.forEach(d => {
-            const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`;
+            const sectionValue = this.toHeader(d);
             // look for if header exists already
             const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString());
             if (existingHeader) {
@@ -161,7 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     };
 
     @computed get allFieldValues() {
-        return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField])));
+        return new Set(this.childDocs.map(doc => (doc[this.notetakingCategoryField] instanceof List ? StrListCast(doc[this.notetakingCategoryField]).join('.') : StrCast(doc[this.notetakingCategoryField]))));
     }
 
     componentDidMount() {
@@ -313,7 +314,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // how to get the width of a document. Currently returns the width of the column (minus margins)
     // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
     getDocWidth = (d: Doc) => {
-        const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType);
+        const heading = this.toHeader(d);
         const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
         const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0;
         const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
@@ -427,7 +428,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
         this.childDocs?.forEach(d => {
             if (d instanceof Promise) return;
-            const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
+            const sectionValue = this.toHeader(d);
             if (sectionValue.toString() === colHeader) {
                 docsMatchingHeader.push(d);
             }
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 1ac0b6d70..a57256424 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -146,7 +146,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
             const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap);
             // just getting the style
-            const style = this.isStackingView ? { margin: undefined, transition: this.getDocTransition(d)(), width: this.columnWidth, marginTop: i ? this.gridGap : 0, height: this.getDocHeight(d)() } : { gridRowEnd: `span ${rowSpan}` };
+            const style = this.isStackingView
+                ? {
+                      //
+                      margin: undefined,
+                      transition: this.getDocTransition(d)(),
+                      width: this.columnWidth,
+                      marginTop: i ? this.gridGap : 0,
+                      height: this.getDocHeight(d)(),
+                      zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0,
+                  }
+                : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 };
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
                 <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
@@ -344,6 +354,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to.
                 rootSelected={this.rootSelected}
                 showTitle={this._props.childlayout_showTitle}
+                showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
                 dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType}
                 onClickScript={this.onChildClickHandler}
                 onDoubleClickScript={this.onChildDoubleClickHandler}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 5c41fee37..6dffb80f1 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -186,21 +186,23 @@ export class CollectionGridView extends CollectionSubView() {
     getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
         return (
             <DocumentView
-                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
-                NativeWidth={returnZero}
-                NativeHeight={returnZero}
-                setContentViewBox={emptyFunction}
                 Document={layout}
                 TemplateDataDocument={layout.resolvedDataDoc as Doc}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                fitWidth={this._props.childLayoutFitWidth}
+                containerViewPath={this.childContainerViewPath}
+                renderDepth={this._props.renderDepth + 1}
                 isContentActive={this.isChildContentActive}
                 PanelWidth={width}
                 PanelHeight={height}
                 ScreenToLocalTransform={dxf}
+                setContentViewBox={emptyFunction}
                 whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
                 onClickScript={this.onChildClickHandler}
-                renderDepth={this._props.renderDepth + 1}
                 dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'}
+                showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
             />
         );
     }
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index d9a01c436..bd8499806 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -10,6 +10,7 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, FireflyStylePresets } from './FireflyConstants';
 
+const DashDropboxId = '2m86iveqdr9vzsa';
 export class DrawingFillHandler {
     static drawingToImage = (drawing: Doc, strength: number, user_prompt: string) => {
         const docData = drawing[DocData];
@@ -33,14 +34,27 @@ export class DrawingFillHandler {
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
                 return imageUrlToBase64(structureUrl)
                     .then(gptDescribeImage)
-                    .then(prompt => 
-                        Networking.PostToServer('/queryFireflyImageFromStructure', 
-                            { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
+                    .then(prompt =>
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structureUrl, strength, styles })
                             .then((info: Upload.ImageInformation) =>
-                                DocumentViewInternal.addDocTabFunc(Docs.Create.ImageDocument(info.accessPaths.agnostic.client, 
-                                    { ai: 'firefly', title: user_prompt || prompt, ai_firefly_prompt: user_prompt || prompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight }), OpenWhere.addRight)
-                        ).catch(e => alert("create image failed: " + e.toString())) 
-                    ); // prettier-ignore
+                                DocumentViewInternal.addDocTabFunc(
+                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                        ai: 'firefly',
+                                        title: user_prompt || prompt,
+                                        ai_firefly_prompt: user_prompt || prompt,
+                                        _width: 500,
+                                        data_nativeWidth: info.nativeWidth,
+                                        data_nativeHeight: info.nativeHeight,
+                                    }),
+                                    OpenWhere.addRight
+                                )
+                            )
+                            .catch(e => {
+                                if (e.toString().includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + e.toString().replace(/^[^"]*/, ''))) {
+                                    window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus();
+                                }
+                            })
+                    ); // prettier-ignore:q
             }
         });
     };
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index a1f8fab8d..6039f5675 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -1,7 +1,9 @@
+import axios from 'axios';
 import { Dropbox } from 'dropbox';
 import * as fs from 'fs';
 import * as multipart from 'parse-multipart-data';
 import * as path from 'path';
+import { DashUserModel } from '../authentication/DashUserModel';
 import { DashUploadUtils } from '../DashUploadUtils';
 import { _error, _invalid, _success, Method } from '../RouteManager';
 import { Directory, filesDirectory } from '../SocketData';
@@ -55,7 +57,7 @@ export default class FireflyManager extends ApiManager {
             )
         );
 
-    uploadImageToDropbox = (fileUrl: string, dbx = new Dropbox({ accessToken: process.env.DROPBOX_TOKEN })) =>
+    uploadImageToDropbox = (fileUrl: string, user: DashUserModel | undefined, dbx = new Dropbox({ accessToken: user?.dropboxToken || '' })) =>
         new Promise<string | Error>((res, rej) =>
             fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => {
                 if (err) {
@@ -69,8 +71,32 @@ export default class FireflyManager extends ApiManager {
                                 .catch(e => res(new Error(e.toString())));
                         })
                         .catch(e => {
-                            console.log('Dropbox error:', e);
-                            res(new Error(e.toString()));
+                            if (user?.dropboxRefresh) {
+                                console.log('*********** try refresh dropbox for: ' + user.email + ' ***********');
+                                this.refreshDropboxToken(user).then(token => {
+                                    if (!token) {
+                                        console.log('Dropbox error: cannot refresh token');
+                                        res(new Error(e.toString()));
+                                    } else {
+                                        const dbxNew = new Dropbox({ accessToken: user.dropboxToken || '' });
+                                        dbxNew
+                                            .filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents })
+                                            .then(response => {
+                                                dbxNew
+                                                    .filesGetTemporaryLink({ path: response.result.path_display ?? '' })
+                                                    .then(link => res(link.result.link))
+                                                    .catch(linkErr => res(new Error(linkErr.toString())));
+                                            })
+                                            .catch(uploadErr => {
+                                                console.log('Dropbox error:', uploadErr);
+                                                res(new Error(uploadErr.toString()));
+                                            });
+                                    }
+                                });
+                            } else {
+                                console.log('Dropbox error:', e);
+                                res(new Error(e.toString()));
+                            }
                         });
                 }
             })
@@ -230,12 +256,35 @@ export default class FireflyManager extends ApiManager {
         );
         return fetched;
     };
+
+    refreshDropboxToken = (user: DashUserModel) =>
+        axios
+            .post(
+                'https://api.dropbox.com/oauth2/token',
+                new URLSearchParams({
+                    refresh_token: user.dropboxRefresh || '',
+                    grant_type: 'refresh_token',
+                    client_id: process.env._CLIENT_DROPBOX_CLIENT_ID || '',
+                    client_secret: process.env._CLIENT_DROPBOX_SECRET || '',
+                }).toString()
+            )
+            .then(refresh => {
+                console.log('***** dropbox token refreshed for ' + user?.email + ' ******* ');
+                user.dropboxToken = refresh.data.access_token;
+                user.save();
+                return user.dropboxToken;
+            })
+            .catch(e => {
+                console.log(e);
+                return undefined;
+            });
+
     protected initialize(register: Registration): void {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImageFromStructure',
             secureHandler: async ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.structureUrl).then(uploadUrl =>
+                this.uploadImageToDropbox(req.body.structureUrl, req.user as DashUserModel).then(uploadUrl =>
                     uploadUrl instanceof Error
                         ? _invalid(res, uploadUrl.message)
                         : this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploadUrl, req.body.strength, req.body.styles).then(fire =>
@@ -275,7 +324,7 @@ export default class FireflyManager extends ApiManager {
             method: Method.POST,
             subscription: '/expandImage',
             secureHandler: ({ req, res }) =>
-                this.uploadImageToDropbox(req.body.file).then(uploadUrl =>
+                this.uploadImageToDropbox(req.body.file, req.user as DashUserModel).then(uploadUrl =>
                     uploadUrl instanceof Error
                         ? _invalid(res, uploadUrl.message)
                         : this.expandImage(uploadUrl, req.body.prompt).then(text => {
@@ -288,5 +337,40 @@ export default class FireflyManager extends ApiManager {
                           })
                 ),
         });
+
+        // construct this url and send user to it.  It will allow them to authorize their dropbox account and will send the resulting token to our endpoint /refreshDropbox
+        // https://www.dropbox.com/oauth2/authorize?client_id=DROPBOX_CLIENT_ID&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox
+        // see: https://dropbox.tech/developers/using-oauth-2-0-with-offline-access
+        //
+        register({
+            method: Method.GET,
+            subscription: '/refreshDropbox',
+            secureHandler: ({ req, res }) => {
+                const user = req.user as DashUserModel;
+                console.log(`******************* dropbox authorized for ${user?.email} ******************`);
+                _success(res, 'dropbox authorized for ' + user?.email);
+
+                const data = new URLSearchParams({
+                    code: req.query.code as string,
+                    grant_type: 'authorization_code',
+                    client_id: process.env._CLIENT_DROPBOX_CLIENT_ID ?? '',
+                    client_secret: process.env._CLIENT_DROPBOX_SECRET ?? '',
+                    redirect_uri: 'http://localhost:1050/refreshDropbox',
+                });
+                axios
+                    .post('https://api.dropbox.com/oauth2/token', data.toString())
+                    .then(response => {
+                        console.log('***** dropbox token (and refresh) received for ' + user?.email + ' ******* ');
+                        user.dropboxToken = response.data.access_token;
+                        user.dropboxRefresh = response.data.refresh_token;
+                        user.save();
+
+                        setTimeout(() => this.refreshDropboxToken(user), response.data.expires_in - 600);
+                    })
+                    .catch(e => {
+                        console.log(e);
+                    });
+            },
+        });
     }
 }
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
index bfa6d7bdb..debeef60c 100644
--- a/src/server/authentication/DashUserModel.ts
+++ b/src/server/authentication/DashUserModel.ts
@@ -9,6 +9,9 @@ export type DashUserModel = mongoose.Document & {
     passwordResetToken?: string;
     passwordResetExpires?: Date;
 
+    dropboxRefresh?: string;
+    dropboxToken?: string;
+
     userDocumentId: string;
     sharingDocumentId: string;
     linkDatabaseId: string;
@@ -37,6 +40,8 @@ const userSchema = new mongoose.Schema(
         passwordResetToken: String,
         passwordResetExpires: Date,
 
+        dropboxRefresh: String,
+        dropboxToken: String,
         userDocumentId: String, // id that identifies a document which hosts all of a user's account data
         sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users
         linkDatabaseId: String,
-- 
cgit v1.2.3-70-g09d2


From b0903079ab0abbc65e927d70ee0f6bb593865869 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 16 Jan 2025 12:00:52 -0500
Subject: fixed style references for generating ai images.  fixed some error
 cases.

---
 .../collectionFreeForm/CollectionFreeFormView.scss |  1 -
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 +-
 src/client/views/nodes/DocumentView.scss           |  1 -
 src/client/views/nodes/ImageBox.tsx                | 56 +++++++-------
 src/client/views/smartdraw/DrawingFillHandler.tsx  | 45 +++++------
 src/server/ApiManagers/FireflyManager.ts           | 86 ++++++++++++----------
 6 files changed, 101 insertions(+), 93 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 82887a7a5..6dabcb8b2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -326,7 +326,6 @@
     .collectionFreeFormView-aiView-regenerate-container {
         text-align: start;
         font-weight: normal;
-        padding: 5px;
         width: 100%;
         display: flex;
         .collectionFreeformView-aiView-subtitle {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0d593f97f..eeb97bf15 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2209,7 +2209,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'}
                             type="text"
                             value={this._drawingFillInput}
-                            onChange={action(e => this._canInteract && (this._drawingFillInput = e.target.value))}
+                            onChange={action(e => {this._drawingFillInput = e.target.value})}
                         />
                         <div className="collectionfreeformview-aiView-strength">
                             Similarity
@@ -2228,7 +2228,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 onChange={action((e, val) => (this._fireflyRefStrength = val as number))}
                                 valueLabelDisplay="auto"
                             />
-                            Reference Strength
                         </div>
                         <Button
                             text="Send"
@@ -2257,7 +2256,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             aria-label="Edit instructions input"
                             type="text"
                             value={this._regenInput}
-                            onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                            onChange={action(e => {this._regenInput = e.target.value})}
                             placeholder="..under development.."
                         />
                         <Button
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index e71f391c6..cd813cb67 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -281,7 +281,6 @@
 
 .documentView-editorView {
     width: 100%;
-    overflow-y: scroll;
     scrollbar-width: thin;
     justify-items: center;
     background-color: rgb(223, 223, 223);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index ec6ce8c2a..82d8ea5f1 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -13,7 +13,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ObjectField } from '../../../fields/ObjectField';
-import { Cast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, ImageCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -540,34 +540,31 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     protected _inputWidth = 50;
     protected _sideBtnMaxPanelPct = 0.12;
     @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+    @observable private _fireflyRefStrength = 0;
     @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
     @computed get maxWidgetSize() { return Math.min(this._btnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width) ? 1 : this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
     @computed get uiBtnScaling() { return this.maxWidgetSize / this._btnWidth; } // prettier-ignore
     @computed get uiInputScaling() { return this.maxWidgetSize / this._inputWidth; } // prettier-ignore
 
-    componentAIViewHistory = () => {
-        const imgs: FireflyImageData[] = this.dataDoc.ai_firefly_history ? JSON.parse(StrCast(this.dataDoc.ai_firefly_history)) : [];
-        return (
-            <div className="imageBox-aiView-history">
-                <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
-                {this._prevImgs.map(img => (
-                    <div key={img.pathname}>
-                        <img
-                            className="imageBox-aiView-img"
-                            src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
-                            onClick={() => {
-                                this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
-                                this.dataDoc.ai_firefly_prompt = img.prompt;
-                                this.dataDoc.ai_firefly_seed = img.seed;
-                            }}
-                        />
-                        <span>{img.prompt}</span>
-                    </div>
-                ))}
-            </div>
-        );
-    };
-    @observable private _fireflyRefStrength = 0;
+    componentAIViewHistory = () => (
+        <div className="imageBox-aiView-history">
+            <Button text="Clear History" type={Type.SEC} size={Size.XSMALL} />
+            {this._prevImgs.map(img => (
+                <div key={img.pathname}>
+                    <img
+                        className="imageBox-aiView-img"
+                        src={ClientUtils.prepend(img.pathname.replace(extname(img.pathname), '_s' + extname(img.pathname)))}
+                        onClick={() => {
+                            this.dataDoc[this.fieldKey] = new ImageField(img.pathname);
+                            this.dataDoc.ai_firefly_prompt = img.prompt;
+                            this.dataDoc.ai_firefly_seed = img.seed;
+                        }}
+                    />
+                    <span>{img.prompt}</span>
+                </div>
+            ))}
+        </div>
+    );
 
     componentAIView = () => {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey]));
@@ -611,7 +608,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             onClick={action(async () => {
                                 this._regenerateLoading = true;
                                 if (this._fireflyRefStrength) {
-                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(
                                         action(() => {
                                             this._regenerateLoading = false;
                                         })
@@ -640,7 +637,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div className="imageBox-aiView-options-container">
                     <span className="imageBox-aiView-subtitle"> More Options: </span>
                     <div className="imageBox-aiView-options">
-                        {showRegenerate && (
+                        {SmartDrawHandler.Instance.ShowRegenerate && (
                             <text className="imageBox-aiView-subtitle" style={{ transform: `scale(${this.uiBtnScaling})` }}>
                                 More Options:
                             </text>
@@ -692,7 +689,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 }).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);
+                                    const genratedDocs = this.Document.generatedDocs
+                                        ? DocCast(this.Document.generatedDocs)
+                                        : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(this.Document.x) + NumCast(this.Document.width), y: NumCast(this.Document.y) });
+                                    Doc.AddDocToList(genratedDocs, undefined, img);
+                                    this.Document[DocData].generatedDocs = genratedDocs;
+                                    if (!DocumentView.getFirstDocumentView(genratedDocs)) this._props.addDocTab(genratedDocs, OpenWhere.addRight);
                                 });
                             }}
                         />
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index cd15d19d5..6f2d1cbe6 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,7 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { DocCast, ImageCast } from '../../../fields/Types';
+import { DocCast, ImageCast, NumCast } from '../../../fields/Types';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
@@ -12,13 +12,15 @@ import { AspectRatioLimits, FireflyDimensionsMap, FireflyImageDimensions, Firefl
 
 const DashDropboxId = '2m86iveqdr9vzsa';
 export class DrawingFillHandler {
-    static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string) => {
+    static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string, styleDoc?: Doc) => {
         const docData = drawing[DocData];
         const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
-        const styleDocs = Doc.Links(drawing)
-            .map(link => Doc.getOppositeAnchor(link, drawing))
-            .map(anchor => anchor && DocCast(anchor.embedContainer));
+        const styleDocs = styleDoc
+            ? [styleDoc]
+            : Doc.Links(drawing)
+                  .map(link => Doc.getOppositeAnchor(link, drawing))
+                  .map(anchor => anchor && DocCast(anchor.embedContainer));
         const styleRef = styleDocs.filter(doc => doc !== undefined && doc.type === 'image').lastElement();
         let styleUrl: string | undefined;
         if (styleRef) {
@@ -47,25 +49,26 @@ export class DrawingFillHandler {
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
                 return imageUrlToBase64(structureUrl)
                     .then(gptDescribeImage)
-                    .then(prompt =>
-                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${user_prompt || prompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
-                            .then((info: Upload.ImageInformation) =>
-                                DocumentViewInternal.addDocTabFunc(
-                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
-                                        ai: 'firefly',
-                                        title: user_prompt || prompt,
-                                        ai_firefly_prompt: user_prompt || prompt,
-                                        _width: 500,
-                                        data_nativeWidth: info.nativeWidth,
-                                        data_nativeHeight: info.nativeHeight,
-                                    }),
-                                    OpenWhere.addRight
-                                )
-                            )
+                    .then((prompt, newPrompt = user_prompt || prompt) =>
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
+                            .then((info: Upload.ImageInformation) => {
+                                const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                    ai: 'firefly',
+                                    title: newPrompt,
+                                    ai_firefly_prompt: newPrompt,
+                                    _width: 500,
+                                    data_nativeWidth: info.nativeWidth,
+                                    data_nativeHeight: info.nativeHeight,
+                                });
+                                const genratedDocs = drawing.generatedDocs ? DocCast(drawing.generatedDocs) : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(drawing.x) + NumCast(drawing.width), y: NumCast(drawing.y) });
+                                drawing[DocData].generatedDocs = genratedDocs;
+                                Doc.AddDocToList(genratedDocs, undefined, img);
+                                if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
+                            })
                             .catch(e => {
                                 if (e.toString().includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + e.toString().replace(/^[^"]*/, ''))) {
                                     window.open(`https://www.dropbox.com/oauth2/authorize?client_id=${DashDropboxId}&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox`, '_blank')?.focus();
-                                }
+                                } else alert(e.toString());
                             })
                     ); // prettier-ignore:q
             }
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 700b275d4..f49cf4132 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -35,8 +35,10 @@ export default class FireflyManager extends ApiManager {
                         ['Authorization', `Bearer ${data.access_token}`],
                     ],
                     body: JSON.stringify({
-                        prompt: prompt,
-                        size: { width: width, height: height },
+                        prompt,
+                        // detailLevel: 'preview', 
+                        // modelVersion: 'image3_fast',
+                        size: { width, height },
                         structure: !structureUrl
                             ? undefined
                             : {
@@ -51,16 +53,18 @@ export default class FireflyManager extends ApiManager {
                             imageReference : !styleUrl 
                                 ? undefined
                                 : {
-                                    source: { uploadId: styleUrl },
+                                    source: { url: styleUrl },
                                 }
                             }
                     }),
                 })
-                    .then(response2 => response2.json().then(json => JSON.stringify((json.outputs?.[0] as { image: { url: string } })?.image)))
-                    .catch(error => {
-                        console.error('Error:', error);
-                        return '';
-                    })
+                .then(response2 => response2.json().then(json => 
+                {
+                    if (json.outputs?.length)
+                        return JSON.stringify((json.outputs?.[0] as { image: { url: string } } ?? {})?.image);
+                    throw new Error(JSON.stringify(json));
+                })
+            )
             )
         );
 
@@ -127,13 +131,7 @@ export default class FireflyManager extends ApiManager {
                     ],
                     body: body,
                 })
-                    .then(response2 =>
-                        response2.json().then(json => {
-                            const seed = json.outputs?.[0]?.seed;
-                            const url = json.outputs?.[0]?.image?.url;
-                            return { seed, url };
-                        })
-                    )
+                    .then(response2 => response2.json().then(json => ({ seed: json.outputs?.[0]?.seed, url: json.outputs?.[0]?.image?.url })))
                     .catch(error => {
                         console.error('Error:', error);
                         return undefined;
@@ -290,33 +288,42 @@ export default class FireflyManager extends ApiManager {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImageFromStructure',
-            secureHandler: async ({ req, res }) => {
-                (req.body.styleRef ? this.uploadImageToDropbox(req.body.styleRef, req.user as DashUserModel) : Promise.resolve(undefined))
-                    .then(styleUrl => {
-                        if (styleUrl instanceof Error) {
-                            _invalid(res, styleUrl.message);
-                            throw new Error('Error uploading images to dropbox');
-                        }
-                        return this.uploadImageToDropbox(req.body.structure, req.user as DashUserModel).then(structureUrl => {
-                            if (structureUrl instanceof Error) {
-                                _invalid(res, structureUrl.message);
+            secureHandler: ({ req, res }) =>
+                new Promise<void>(resolver => {
+                    (req.body.styleUrl ? this.uploadImageToDropbox(req.body.styleUrl, req.user as DashUserModel) : Promise.resolve(undefined))
+                        .then(styleUrl => {
+                            if (styleUrl instanceof Error) {
+                                _invalid(res, styleUrl.message);
                                 throw new Error('Error uploading images to dropbox');
                             }
-                            return { styleUrl, structureUrl };
-                        });
-                    })
-                    .then(uploads =>
-                        this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, req.body.styleUrl).then(fire => {
-                            DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                                if (info instanceof Error) _invalid(res, info.message);
-                                else _success(res, info);
-                            });
+                            this.uploadImageToDropbox(req.body.structure, req.user as DashUserModel)
+                                .then(structureUrl => {
+                                    if (structureUrl instanceof Error) {
+                                        _invalid(res, structureUrl.message);
+                                        throw new Error('Error uploading images to dropbox');
+                                    }
+                                    return { styleUrl, structureUrl };
+                                })
+                                .then(uploads =>
+                                    this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl)
+                                        .then(fire =>
+                                            DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
+                                                if (info instanceof Error) _invalid(res, info.message);
+                                                else _success(res, info);
+                                                resolver();
+                                            })
+                                        )
+                                        .catch(e => {
+                                            _invalid(res, e.message);
+                                            resolver();
+                                        })
+                                );
                         })
-                    )
-                    .catch(() => {
-                        /* do nothing */
-                    });
-            },
+                        .catch(() => {
+                            /* do nothing */
+                            resolver();
+                        });
+                }),
         });
         register({
             method: Method.POST,
@@ -333,7 +340,6 @@ export default class FireflyManager extends ApiManager {
         register({
             method: Method.POST,
             subscription: '/queryFireflyImageText',
-            // eslint-disable-next-line @typescript-eslint/no-unused-vars
             secureHandler: ({ req, res }) =>
                 fetch(req.body.file).then(json =>
                     json.blob().then(file =>
-- 
cgit v1.2.3-70-g09d2


From fdc8741187489963353938072f05051b8986c400 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 16 Jan 2025 18:03:03 -0500
Subject: fixes for stlye refs in firefly generate

---
 src/client/views/smartdraw/DrawingFillHandler.tsx | 50 +++++++++++------------
 1 file changed, 25 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 6f2d1cbe6..16ac2b79e 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,7 +1,8 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
-import { Doc } from '../../../fields/Doc';
+import { Doc, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
@@ -14,23 +15,19 @@ const DashDropboxId = '2m86iveqdr9vzsa';
 export class DrawingFillHandler {
     static drawingToImage = async (drawing: Doc, strength: number, user_prompt: string, styleDoc?: Doc) => {
         const docData = drawing[DocData];
-        const tags: string[] = ((docData?.tags as unknown as string[]) ?? []).map(tag => tag.slice(1)) ?? [];
+        const tags = StrListCast(docData.tags).map(tag => tag.slice(1));
         const styles = tags.filter(tag => FireflyStylePresets.has(tag));
-        const styleDocs = styleDoc
-            ? [styleDoc]
+        const styleDocs = !Doc.Links(drawing).length
+            ? styleDoc && !tags.length
+                ? [styleDoc]
+                : []
             : Doc.Links(drawing)
                   .map(link => Doc.getOppositeAnchor(link, drawing))
                   .map(anchor => anchor && DocCast(anchor.embedContainer));
-        const styleRef = styleDocs.filter(doc => doc !== undefined && doc.type === 'image').lastElement();
-        let styleUrl: string | undefined;
-        if (styleRef) {
-            const styleImg = await DocumentView.GetDocImage(styleRef);
-            if (styleImg) {
-                const { href } = ImageCast(styleImg).url;
-                const hrefParts = href.split('.');
-                styleUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
-            }
-        }
+        const styleUrl = await DocumentView.GetDocImage(styleDocs.filter(doc => doc?.data instanceof ImageField).lastElement())?.then(styleImg => {
+            const hrefParts = ImageCast(styleImg).url.href.split('.');
+            return `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
+        });
         DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
@@ -52,17 +49,20 @@ export class DrawingFillHandler {
                     .then((prompt, newPrompt = user_prompt || prompt) =>
                         Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
                             .then((info: Upload.ImageInformation) => {
-                                const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
-                                    ai: 'firefly',
-                                    title: newPrompt,
-                                    ai_firefly_prompt: newPrompt,
-                                    _width: 500,
-                                    data_nativeWidth: info.nativeWidth,
-                                    data_nativeHeight: info.nativeHeight,
-                                });
-                                const genratedDocs = drawing.generatedDocs ? DocCast(drawing.generatedDocs) : Docs.Create.MasonryDocument([], { _width: 400, _height: 400, x: NumCast(drawing.x) + NumCast(drawing.width), y: NumCast(drawing.y) });
-                                drawing[DocData].generatedDocs = genratedDocs;
-                                Doc.AddDocToList(genratedDocs, undefined, img);
+                                const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 });
+                                drawing[DocData].ai_firefly_generatedDocs = genratedDocs;
+                                Doc.AddDocToList(
+                                    genratedDocs,
+                                    undefined,
+                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                        ai: 'firefly',
+                                        title: newPrompt,
+                                        ai_firefly_prompt: newPrompt,
+                                        _width: 500,
+                                        data_nativeWidth: info.nativeWidth,
+                                        data_nativeHeight: info.nativeHeight,
+                                    })
+                                );
                                 if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
                             })
                             .catch(e => {
-- 
cgit v1.2.3-70-g09d2


From 156a2040cc51397c3656615f9133279dac900c82 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 17 Jan 2025 11:17:15 -0500
Subject: changed to generating 4 images for each firefly request.

---
 src/client/views/nodes/DocumentView.tsx           |  4 ++--
 src/client/views/smartdraw/DrawingFillHandler.tsx | 28 ++++++++++++-----------
 src/server/ApiManagers/FireflyManager.ts          | 22 ++++++++++--------
 3 files changed, 29 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e37658ca5..bc3f27124 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1110,10 +1110,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      * @param doc Doc to snapshot
      * @returns promise of icon ImageField
      */
-    public static GetDocImage(doc: Doc) {
+    public static GetDocImage(doc?: Doc) {
         return DocumentView.getDocumentView(doc)
             ?.ComponentView?.updateIcon?.()
-            .then(() => ImageCast(doc?.icon, ImageCast(doc[Doc.LayoutFieldKey(doc)])));
+            .then(() => ImageCast(doc!.icon, ImageCast(doc![Doc.LayoutFieldKey(doc!)])));
     }
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 16ac2b79e..d0a840883 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,7 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { DocCast, ImageCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { gptDescribeImage } from '../../apis/gpt/GPT';
@@ -48,20 +48,22 @@ export class DrawingFillHandler {
                     .then(gptDescribeImage)
                     .then((prompt, newPrompt = user_prompt || prompt) =>
                         Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
-                            .then((info: Upload.ImageInformation) => {
+                            .then((infos: Upload.ImageInformation[]) => {
                                 const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 });
                                 drawing[DocData].ai_firefly_generatedDocs = genratedDocs;
-                                Doc.AddDocToList(
-                                    genratedDocs,
-                                    undefined,
-                                    Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
-                                        ai: 'firefly',
-                                        title: newPrompt,
-                                        ai_firefly_prompt: newPrompt,
-                                        _width: 500,
-                                        data_nativeWidth: info.nativeWidth,
-                                        data_nativeHeight: info.nativeHeight,
-                                    })
+                                infos.map(info =>
+                                    Doc.AddDocToList(
+                                        genratedDocs,
+                                        undefined,
+                                        Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
+                                            ai: 'firefly',
+                                            title: newPrompt,
+                                            ai_firefly_prompt: newPrompt,
+                                            _width: 500,
+                                            data_nativeWidth: info.nativeWidth,
+                                            data_nativeHeight: info.nativeHeight,
+                                        })
+                                    )
                                 );
                                 if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
                             })
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index f49cf4132..160a94d40 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -36,8 +36,9 @@ export default class FireflyManager extends ApiManager {
                     ],
                     body: JSON.stringify({
                         prompt,
-                        // detailLevel: 'preview', 
-                        // modelVersion: 'image3_fast',
+                        numVariations: 4,
+                        detailLevel: 'preview', 
+                        modelVersion: 'image3_fast',
                         size: { width, height },
                         structure: !structureUrl
                             ? undefined
@@ -61,7 +62,7 @@ export default class FireflyManager extends ApiManager {
                 .then(response2 => response2.json().then(json => 
                 {
                     if (json.outputs?.length)
-                        return JSON.stringify((json.outputs?.[0] as { image: { url: string } } ?? {})?.image);
+                        return (json.outputs as {image: {url:string }}[]).map(output => output.image);
                     throw new Error(JSON.stringify(json));
                 })
             )
@@ -306,13 +307,14 @@ export default class FireflyManager extends ApiManager {
                                 })
                                 .then(uploads =>
                                     this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl)
-                                        .then(fire =>
-                                            DashUploadUtils.UploadImage(JSON.parse(fire ?? '').url).then(info => {
-                                                if (info instanceof Error) _invalid(res, info.message);
-                                                else _success(res, info);
-                                                resolver();
-                                            })
-                                        )
+                                        .then(images => {
+                                            Promise.all((images ?? [new Error('no images were generated')]).map(fire => (fire instanceof Error ? fire : DashUploadUtils.UploadImage(fire.url))))
+                                                .then(dashImages => {
+                                                    if (dashImages.every(img => img instanceof Error)) _invalid(res, dashImages[0]!.message);
+                                                    else _success(res, JSON.stringify(dashImages.filter(img => !(img instanceof Error))));
+                                                })
+                                                .then(resolver);
+                                        })
                                         .catch(e => {
                                             _invalid(res, e.message);
                                             resolver();
-- 
cgit v1.2.3-70-g09d2


From a162057114fd3d961e447d6c3051c1c8c0c6b46d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 26 Feb 2025 23:41:07 -0500
Subject: made ai. button draggable to regenerate images.

---
 src/ClientUtils.ts                                 |  2 +-
 src/client/views/DocumentButtonBar.tsx             | 18 +++++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/ImageBox.tsx                |  6 +--
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  6 +--
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 51 +++++++++++++++++++---
 src/server/ApiManagers/FireflyManager.ts           | 10 ++---
 7 files changed, 72 insertions(+), 23 deletions(-)

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

diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index c5b61477b..e1f490c1a 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -599,7 +599,7 @@ export function StopEvent(e: React.PointerEvent | React.MouseEvent | React.Keybo
 
 export function setupMoveUpEvents(
     target: object,
-    e: React.PointerEvent,
+    e: React.PointerEvent | PointerEvent,
     moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
     upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => void,
     clickEvent: (e: PointerEvent, doubleTap?: boolean) => unknown,
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index a9f03a658..b17dbc93d 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -28,6 +28,7 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
 
 @observer
 export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: unknown }> {
@@ -319,7 +320,22 @@ 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={undoable(() => this.view0?.toggleAIEditor(), 'toggle AI editor')}>
+                <div
+                    className="documentButtonBar-icon"
+                    style={{ color: 'white' }}
+                    onPointerDown={e =>
+                        setupMoveUpEvents(
+                            this,
+                            e,
+                            me => {
+                                this.view0?.docViewPath().slice(-2)[0]?.ComponentView?.showSmartDraw?.(me.x, me.y, true);
+                                SmartDrawHandler.Instance.startDragging(me);
+                                return true;
+                            },
+                            emptyFunction,
+                            undoable(() => this.view0?.toggleAIEditor(), 'toggle AI editor')
+                        )
+                    }>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="robot" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 752e8b217..aa9b9b0fa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1294,7 +1294,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      */
     addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => {
         const docData = doc[DocData];
-        docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+        docData.title = opts.text;
         docData._width = opts.size;
         docData.ai_drawing_input = opts.text;
         docData.ai_drawing_complexity = opts.complexity;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f3aea76a5..55474cb7e 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -625,11 +625,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             onClick={action(async () => {
                                 this._regenerateLoading = true;
                                 if (this._fireflyRefStrength) {
-                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(
-                                        action(() => {
-                                            this._regenerateLoading = false;
-                                        })
-                                    );
+                                    DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title), this.Document)?.then(action(() => (this._regenerateLoading = false)));
                                 } else {
                                     SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
                                         action(newImgs => {
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index d0a840883..7447f8afb 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -47,11 +47,11 @@ export class DrawingFillHandler {
                 return imageUrlToBase64(structureUrl)
                     .then(gptDescribeImage)
                     .then((prompt, newPrompt = user_prompt || prompt) =>
-                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structure: structureUrl, strength, presets: styles, styleUrl })
-                            .then((infos: Upload.ImageInformation[]) => {
+                        Networking.PostToServer('/queryFireflyImageFromStructure', { prompt: `${newPrompt}`, width: dims.width, height: dims.height, structureUrl, strength, presets: styles, styleUrl })
+                            .then(res => {
                                 const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { _width: 400, _height: 400 });
                                 drawing[DocData].ai_firefly_generatedDocs = genratedDocs;
-                                infos.map(info =>
+                                (res as Upload.ImageInformation[]).map(info =>
                                     Doc.AddDocToList(
                                         genratedDocs,
                                         undefined,
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 811bc6e25..fbf471900 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -7,7 +7,7 @@ import React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
 import { INode, parse } from 'svgson';
-import { imageUrlToBase64 } from '../../../ClientUtils';
+import { imageUrlToBase64, setupMoveUpEvents } from '../../../ClientUtils';
 import { unimplementedFunction } from '../../../Utils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
@@ -205,11 +205,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._isLoading = true;
         this._canInteract = false;
         if (this.ShowRegenerate) {
-            await this.regenerate(this._selectedDocs).then(
-                action(() => {
-                    this._showEditBox = false;
-                })
-            );
+            await this.regenerate(this._selectedDocs, undefined, undefined, this._regenInput).then(action(() => (this._showEditBox = false)));
         } else {
             this._showOptions = false;
             try {
@@ -295,7 +291,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 }
                 const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)?.[1];
                 const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
-                    title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
+                    title: input,
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
                     ai: 'firefly',
@@ -523,6 +519,19 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         return (
             <div
                 className="smart-draw-handler"
+                onPointerDown={e =>
+                    setupMoveUpEvents(
+                        this,
+                        e,
+                        action(me => {
+                            this._pageX = this._pageX + me.movementX;
+                            this._pageY = this._pageY + me.movementY;
+                            return false;
+                        }),
+                        () => {},
+                        () => {}
+                    )
+                }
                 style={{
                     display: this._display ? '' : 'none',
                     left: this._pageX,
@@ -581,6 +590,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
                 onKeyDown={this.handleKeyPress}
                 placeholder="Edit instructions"
+                onPointerDown={e => e.stopPropagation()}
             />
             <Button
                 style={{ alignSelf: 'flex-end' }}
@@ -593,10 +603,37 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         </div>
     );
 
+    startDragging = (e: PointerEvent) => {
+        setupMoveUpEvents(
+            this,
+            e,
+            action(me => {
+                this._pageX = this._pageX + me.movementX;
+                this._pageY = this._pageY + me.movementY;
+                return false;
+            }),
+            () => {},
+            () => {}
+        );
+    };
     renderRegenerate = () => (
         <div
             className="smart-draw-handler"
+            onPointerDown={e =>
+                setupMoveUpEvents(
+                    this,
+                    e,
+                    action(me => {
+                        this._pageX = this._pageX + me.movementX;
+                        this._pageY = this._pageY + me.movementY;
+                        return false;
+                    }),
+                    () => {},
+                    () => {}
+                )
+            }
             style={{
+                padding: 10,
                 left: this._pageX,
                 ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
                 background: SettingsManager.userBackgroundColor,
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts
index 8a310aed8..e75ede9df 100644
--- a/src/server/ApiManagers/FireflyManager.ts
+++ b/src/server/ApiManagers/FireflyManager.ts
@@ -298,13 +298,13 @@ export default class FireflyManager extends ApiManager {
                                 _invalid(res, styleUrl.message);
                                 throw new Error('Error uploading images to dropbox');
                             }
-                            this.uploadImageToDropbox(req.body.structure, req.user as DashUserModel)
-                                .then(structureUrl => {
-                                    if (structureUrl instanceof Error) {
-                                        _invalid(res, structureUrl.message);
+                            this.uploadImageToDropbox(req.body.structureUrl, req.user as DashUserModel)
+                                .then(dropboxStructureUrl => {
+                                    if (dropboxStructureUrl instanceof Error) {
+                                        _invalid(res, dropboxStructureUrl.message);
                                         throw new Error('Error uploading images to dropbox');
                                     }
-                                    return { styleUrl, structureUrl };
+                                    return { styleUrl, structureUrl: dropboxStructureUrl };
                                 })
                                 .then(uploads =>
                                     this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl)
-- 
cgit v1.2.3-70-g09d2


From 532f0fa22281fef1e35e3d0a6064ee57e4673253 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 28 Feb 2025 14:52:39 -0500
Subject: added drop target for regenerating images

---
 src/client/util/DragManager.ts                    |  3 +
 src/client/views/nodes/ImageBox.scss              |  8 ++
 src/client/views/nodes/ImageBox.tsx               | 90 +++++++++++++++--------
 src/client/views/smartdraw/DrawingFillHandler.tsx | 23 +++---
 4 files changed, 83 insertions(+), 41 deletions(-)

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

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 81ea840f1..6c8179c82 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -555,9 +555,12 @@ export namespace DragManager {
         let scrollAwaiter: Opt<NodeJS.Timeout>;
 
         let startWindowDragTimer: NodeJS.Timeout | undefined;
+        let startCanEmbed = SnappingManager.CanEmbed;
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (docDragData) {
+                if (e.ctrlKey) SnappingManager.SetCanEmbed(true);
+                else if (!startCanEmbed) SnappingManager.SetCanEmbed(false);
                 docDragData.userDropAction = e.ctrlKey && e.altKey ? dropActionType.copy : e.shiftKey ? dropActionType.move : e.ctrlKey ? dropActionType.embed : docDragData.defaultDropAction;
                 const targClassName = e.target instanceof HTMLElement && typeof e.target.className === 'string' ? e.target.className : '';
                 if (['lm_tab', 'lm_title_wrap', 'lm_tabs', 'lm_header'].includes(targClassName) && docDragData.draggedDocuments.length === 1) {
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 59e093683..671621bbe 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -122,6 +122,7 @@
         }
     }
 }
+.imageBox-regenerateDropTarget,
 .imageBox-alternateDropTarget {
     position: absolute;
     color: white;
@@ -136,6 +137,13 @@
         height: 100%;
     }
 }
+.imageBox-regenerateDropTarget {
+    right: 30;
+    border-radius: 50%;
+    svg {
+        border-radius: 50%;
+    }
+}
 
 .imageBox-fader img {
     position: absolute;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 55474cb7e..d122ca5b0 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -36,7 +36,7 @@ import { OverlayView } from '../OverlayView';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
-import { FireflyImageData, isFireflyImageData } from '../smartdraw/FireflyConstants';
+import { FireflyImageData, FireflyImageDimensions, isFireflyImageData } from '../smartdraw/FireflyConstants';
 import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { StickerPalette } from '../smartdraw/StickerPalette';
 import { StyleProp } from '../StyleProp';
@@ -45,6 +45,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
+import { RichTextField } from '../../../fields/RichTextField';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -83,6 +84,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
     private _overlayIconRef = React.createRef<HTMLDivElement>();
+    private _regenerateIconRef = React.createRef<HTMLDivElement>();
     private _mainCont: HTMLDivElement | null = null;
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     imageRef: HTMLImageElement | null = null; // <video> ref
@@ -196,36 +198,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._searchInput = selection;
     };
 
-    drop = undoable((e: Event, de: DragManager.DropEvent) => {
-        if (de.complete.docDragData) {
-            let added: boolean | undefined;
-            const targetIsBullseye = (ele: HTMLElement): boolean => {
-                if (!ele) return false;
-                if (ele === this._overlayIconRef.current) return true;
-                return targetIsBullseye(ele.parentElement as HTMLElement);
-            };
-            if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) {
-                added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => {
-                    this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
-                    return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop);
-                }, true);
-            } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
-                const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
-                const targetField = Doc.LayoutFieldKey(layoutDoc);
-                const targetDoc = layoutDoc[DocData];
-                if (targetDoc[targetField] instanceof ImageField) {
-                    added = true;
-                    this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
-                    Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
-                    Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
+    drop = undoable(
+        action((e: Event, de: DragManager.DropEvent) => {
+            if (de.complete.docDragData) {
+                let added: boolean | undefined;
+                const hitDropTarget = (ele: HTMLElement, dropTarget: HTMLDivElement | null): boolean => {
+                    if (!ele) return false;
+                    if (ele === dropTarget) return true;
+                    return hitDropTarget(ele.parentElement as HTMLElement, dropTarget);
+                };
+                if (de.metaKey || hitDropTarget(e.target as HTMLElement, this._overlayIconRef.current)) {
+                    added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => {
+                        this.layoutDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
+                        return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_alternates', drop);
+                    }, true);
+                } else if (hitDropTarget(e.target as HTMLElement, this._regenerateIconRef.current)) {
+                    this._regenerateLoading = true;
+                    const drag = de.complete.docDragData.draggedDocuments.lastElement();
+                    const dragField = drag[Doc.LayoutFieldKey(drag)];
+                    const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title));
+                    const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text);
+                    DrawingFillHandler.drawingToImage(this.Document, 100, newPrompt(dragField instanceof RichTextField ? dragField.Text : ''), drag)?.then(action(() => (this._regenerateLoading = false)));
+                    added = false;
+                } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
+                    const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
+                    const targetField = Doc.LayoutFieldKey(layoutDoc);
+                    const targetDoc = layoutDoc[DocData];
+                    if (targetDoc[targetField] instanceof ImageField) {
+                        added = true;
+                        this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
+                        Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
+                        Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
+                    }
                 }
+                added === false && e.preventDefault();
+                added !== undefined && e.stopPropagation();
+                return added;
             }
-            added === false && e.preventDefault();
-            added !== undefined && e.stopPropagation();
-            return added;
-        }
-        return false;
-    }, 'image drop');
+            return false;
+        }),
+        'image drop'
+    );
 
     @undoBatch
     resolution = () => {
@@ -485,6 +498,24 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             </Tooltip>
         );
     }
+    @computed get regenerateImageIcon() {
+        return (
+            <div
+                className="imageBox-regenerateDropTarget"
+                ref={this._regenerateIconRef}
+                onClick={() => DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs), { openLocation: OpenWhere.addRight })}
+                style={{
+                    display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none',
+                    transform: `scale(${this.uiBtnScaling})`,
+                    width: this._sideBtnWidth,
+                    height: this._sideBtnWidth,
+                    background: 'transparent',
+                    // color: SettingsManager.userBackgroundColor,
+                }}>
+                {this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width="100%" height="100%" /> : <FontAwesomeIcon icon="portrait" color={SettingsManager.userColor} size="lg" />}
+            </div>
+        );
+    }
 
     @computed get paths() {
         const field = this.dataDoc[this.fieldKey] instanceof ImageField ? Cast(this.dataDoc[this.fieldKey], ImageField, null) : new ImageField(String(this.dataDoc[this.fieldKey])); // retrieve the primary image URL that is being rendered from the data doc
@@ -807,6 +838,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         <ReactLoading type="spin" height={50} width={50} color={'blue'} />
                     </div>
                 ) : null}
+                {this.regenerateImageIcon}
                 {this.overlayImageIcon}
                 {this.annotationLayer}
                 {!this._mainCont || !this.DocumentView || !this._annotationLayer.current ? null : (
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 7447f8afb..0a30b14b8 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -28,19 +28,15 @@ export class DrawingFillHandler {
             const hrefParts = ImageCast(styleImg).url.href.split('.');
             return `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
         });
-        DocumentView.GetDocImage(drawing)?.then(imageField => {
+        return DocumentView.GetDocImage(drawing)?.then(imageField => {
             if (imageField) {
                 const aspectRatio = (drawing.width as number) / (drawing.height as number);
-                let dims: { width: number; height: number };
-                if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) {
-                    dims = FireflyDimensionsMap[FireflyImageDimensions.Widescreen];
-                } else if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Landscape]) {
-                    dims = FireflyDimensionsMap[FireflyImageDimensions.Landscape];
-                } else if (aspectRatio < AspectRatioLimits[FireflyImageDimensions.Portrait]) {
-                    dims = FireflyDimensionsMap[FireflyImageDimensions.Portrait];
-                } else {
-                    dims = FireflyDimensionsMap[FireflyImageDimensions.Square];
-                }
+                const dims = (() => {
+                    if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Widescreen]) return FireflyDimensionsMap[FireflyImageDimensions.Widescreen];
+                    if (aspectRatio > AspectRatioLimits[FireflyImageDimensions.Landscape]) return FireflyDimensionsMap[FireflyImageDimensions.Landscape];
+                    if (aspectRatio < AspectRatioLimits[FireflyImageDimensions.Portrait]) return FireflyDimensionsMap[FireflyImageDimensions.Portrait];
+                    return FireflyDimensionsMap[FireflyImageDimensions.Square];
+                })();
                 const { href } = ImageCast(imageField).url;
                 const hrefParts = href.split('.');
                 const structureUrl = `${hrefParts.slice(0, -1).join('.')}_o.${hrefParts.lastElement()}`;
@@ -62,7 +58,10 @@ export class DrawingFillHandler {
                                             _width: 500,
                                             data_nativeWidth: info.nativeWidth,
                                             data_nativeHeight: info.nativeHeight,
-                                        })
+                                        }),
+                                        undefined,
+                                        undefined,
+                                        true
                                     )
                                 );
                                 if (!DocumentView.getFirstDocumentView(genratedDocs)) DocumentViewInternal.addDocTabFunc(genratedDocs, OpenWhere.addRight);
-- 
cgit v1.2.3-70-g09d2


From db3bdb19ff7bc1c69c544797c05a6db3b72b1464 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 5 Mar 2025 14:54:18 -0500
Subject: added:hover mode for images to make primary image fade in instead of
 alternate.

---
 src/client/documents/Documents.ts                  |  3 ++
 src/client/views/MainView.tsx                      |  2 +-
 src/client/views/TagsView.tsx                      |  3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/DocumentView.scss           |  1 +
 src/client/views/nodes/DocumentView.tsx            | 16 +++++----
 src/client/views/nodes/ImageBox.scss               |  6 ++++
 src/client/views/nodes/ImageBox.tsx                | 40 +++++++++++++++-------
 .../views/nodes/chatbot/tools/ImageCreationTool.ts |  3 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |  3 ++
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  4 +++
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  8 +++--
 12 files changed, 65 insertions(+), 26 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1ce25165c..21d3c978b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -384,6 +384,9 @@ export class DocumentOptions {
     presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
     presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out', false);
 
+    data_annotations?: List<Doc>;
+    _data_usePath?: STRt = new StrInfo("description of field key to display in image box ('alternate','alternate:hover', 'data:hover'). defaults to primary", false);
+    data_alternates?: List<Doc>;
     data?: FieldType;
     data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page');
     _face_showImages?: BOOLt = new BoolInfo('whether to show images in uniqe face Doc');
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index afefe3f03..cc7c1a42b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -686,7 +686,7 @@ export class MainView extends ObservableReactComponent<object> {
         );
     }
     @computed get mainDocView() {
-        const headerBar = null; // this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView;
+        const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView;
         return (
             <>
                 {headerBar}
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index b70e21918..93d6fb684 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -398,8 +398,7 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
                                         e.stopPropagation();
                                     }}
                                     type="text"
-                                    placeholder="Input tags for document..."
-                                    aria-label="tagsView-input"
+                                    placeholder="Enter #tags or @metadata"
                                     className="tagsView-input"
                                     style={{ width: '100%', borderRadius: '5px' }}
                                 />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b3d908da4..43addfc29 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -344,7 +344,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     focusOnPoint = (options: FocusViewOptions) => {
         const { pointFocus, zoomTime, didMove } = options;
         if (!this.Document.isGroup && pointFocus && !didMove) {
-            const dfltScale = this.isAnnotationOverlay ? 1 : 0.5;
+            const dfltScale = this.isAnnotationOverlay ? 1 : 0.25;
             if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) {
                 this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime);
                 options.didMove = true;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 294af4d96..dd5fd0d0c 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -291,6 +291,7 @@
     justify-items: center;
     background-color: rgb(223, 223, 223);
     transform-origin: top left;
+    background: transparent;
 
     .documentView-editorView-resizer {
         height: 5px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5f5dd1210..595abc7f8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -279,16 +279,17 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
     };
     onBrowseClick = (e: React.MouseEvent) => {
-        const browseTransitionTime = 500;
+        //const browseTransitionTime = 500;
         DocumentView.DeselectAll();
         DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
-            const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
+            // const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
             if (!focused && this._docView) {
-                this._docView
-                    .docViewPath()
-                    .reverse()
-                    .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options));
-                Doc.linkFollowHighlight(this.Document, false);
+                DocumentView.showDocument(this.Document, { zoomScale: 0.3, willZoomCentered: true });
+                // this._docView
+                //     .docViewPath()
+                //     .reverse()
+                //     .forEach(cont => cont.ComponentView?.focus?.(cont.Document, options));
+                // Doc.linkFollowHighlight(this.Document, false);
             }
         });
         e.stopPropagation();
@@ -797,6 +798,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                         <div
                             className="documentView-editorView"
                             style={{
+                                background: SnappingManager.userVariantColor,
                                 width: `${100 / this.uiBtnScaling}%`, //
                                 transform: `scale(${this.uiBtnScaling})`,
                             }}
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 15581a9e8..3d6942e6f 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -40,6 +40,7 @@
     max-height: 100%;
     pointer-events: inherit;
     background: transparent;
+    z-index: 0;
     // z-index: -10000; // bcz: not sure why this was here.  it broke dropping images on the image box alternate bullseye icon.
 
     img {
@@ -103,6 +104,10 @@
     margin: 0 auto;
     display: flex;
     height: 100%;
+    img {
+        object-fit: contain;
+        height: 100%;
+    }
 
     .imageBox-fadeBlocker,
     .imageBox-fadeBlocker-hover {
@@ -237,5 +242,6 @@
         text-overflow: ellipsis;
         max-width: 65%;
         width: 100%;
+        color: black;
     }
 }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4979a4d4d..017ef7191 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -436,6 +436,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
     getScrollHeight = () => (this._props.fitWidth?.(this.Document) !== false && NumCast(this.layoutDoc._freeform_scale, 1) === NumCast(this.dataDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
 
+    @computed get usingAlternate() {
+        const usePath = StrCast(this.Document[this.fieldKey + '_usePath']);
+        return 'alternate' === usePath || ('alternate:hover' === usePath && this._isHovering) || (':hover' === usePath && !this._isHovering);
+    }
+
     @computed get nativeSize() {
         TraceMobx();
         if (this.paths.length && this.paths[0].includes('icon-hi')) return { nativeWidth: NumCast(this.layoutDoc._width), nativeHeight: NumCast(this.layoutDoc._height), nativeOrientation: 0 };
@@ -471,10 +476,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
                             <em>alternate, </em>
                         </span>
-                        and show
                         <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
                             <em> alternate on hover</em>
                         </span>
+                        and show
+                        <span style={{ color: usePath === ':hover' ? 'black' : undefined }}>
+                            <em> primary on hover</em>
+                        </span>
                     </div>
                 }>
                 <div
@@ -482,7 +490,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     ref={this._overlayIconRef}
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
-                            this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
+                            this.layoutDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : usePath === 'alternate:hover' ? ':hover' : undefined;
                         })
                     }
                     style={{
@@ -527,7 +535,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 .filter(url => url)
                 .map(url => this.choosePath(url)) ?? []; // acc  ess the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
-        return paths.length ? paths : [defaultUrl.href];
+        return paths.length ? paths.reverse() : [defaultUrl.href];
     }
 
     @computed get content() {
@@ -552,7 +560,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             transformOrigin = 'right top';
             transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`;
         }
-        const usePath = this.layoutDoc[`_${this.fieldKey}_usePath`];
 
         return (
             <div
@@ -571,7 +578,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         ref={action((r: HTMLImageElement | null) => (this.imageRef = r))}
                         key="paths"
                         src={srcpath}
-                        style={{ transform, transformOrigin, objectFit: 'fill', height: '100%' }}
+                        style={{ transform, transformOrigin }}
                         onError={action(e => {
                             this._error = e.toString();
                         })}
@@ -579,7 +586,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         width={nativeWidth}
                     />
                     {fadepath === srcpath ? null : (
-                        <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
+                        <div className={`imageBox-fadeBlocker${this.usingAlternate ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
                             <img alt="" className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                         </div>
                     )}
@@ -619,8 +626,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return (
             <div className="imageBox-aiView">
                 <div className="imageBox-aiView-regenerate">
-                    <span className="imageBox-aiView-firefly">Firefly:</span>
+                    <span className="imageBox-aiView-firefly" style={{ color: SnappingManager.userColor }}>
+                        Firefly:
+                    </span>
                     <input
+                        style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
                         className="imageBox-aiView-input"
                         aria-label="Edit instructions input"
                         type="text"
@@ -631,7 +641,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     <div className="imageBox-aiView-regenerate-createBtn">
                         <Button
                             text="Create"
-                            type={Type.SEC}
+                            type={Type.TERT}
+                            color={SnappingManager.userColor}
+                            background={SnappingManager.userBackgroundColor}
                             // style={{ alignSelf: 'flex-end' }}
                             icon={this._regenerateLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
@@ -663,13 +675,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     </div>
                 </div>
                 <div className="imageBox-aiView-strength">
-                    <span className="imageBox-aiView-similarity">Similarity</span>
+                    <span className="imageBox-aiView-similarity" style={{ color: SnappingManager.userColor }}>
+                        Similarity
+                    </span>
                     <Slider
                         className="imageBox-aiView-slider"
                         sx={{
-                            '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+                            '& .MuiSlider-track': { color: SettingsManager.userColor },
                             '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
-                            '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+                            '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
                         }}
                         min={0}
                         max={100}
@@ -725,11 +739,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
-
     render() {
         TraceMobx();
         const borderRad = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BorderRounding) as string;
         const borderRadius = borderRad?.includes('px') ? `${Number(borderRad.split('px')[0]) / (this._props.NativeDimScaling?.() || 1)}px` : borderRad;
+        const alts = DocListCast(this.dataDoc[this.fieldKey + '_alternates']);
+        const doc = this.usingAlternate ? (alts.lastElement() ?? this.Document) : this.Document;
         return (
             <div
                 className="imageBox"
@@ -754,6 +769,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <CollectionFreeFormView
                     ref={this._ffref}
                     {...this._props}
+                    Document={doc}
                     setContentViewBox={emptyFunction}
                     NativeWidth={returnZero}
                     NativeHeight={returnZero}
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index e92d87dfd..37907fd4f 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -5,6 +5,7 @@ import { ParametersType, ToolInfo } from '../types/tool_types';
 import { Observation } from '../types/types';
 import { BaseTool } from './BaseTool';
 import { Upload } from '../../../../../server/SharedMediaTypes';
+import { List } from '../../../../../fields/List';
 
 const imageCreationToolParams = [
     {
@@ -41,7 +42,7 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
                 image_prompt,
             })) as { result: Upload.FileInformation & Upload.InspectionResults; url: string };
             console.log('Image generation result:', result);
-            this._createImage(result, { text: RTFCast(image_prompt) });
+            this._createImage(result, { text: RTFCast(image_prompt), ai: 'dall-e-3', tags: new List<string>(['@ai']) });
             return url
                 ? [
                       {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 79f5121ed..4dc45e6a0 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -30,6 +30,7 @@ import { Upload } from '../../../../server/SharedMediaTypes';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler';
 import { ImageField } from '../../../../fields/URLField';
+import { List } from '../../../../fields/List';
 
 export enum GPTPopupMode {
     SUMMARY, // summary of seleted document text
@@ -352,6 +353,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 y: NumCast(textAnchor.y),
                 _height: 200,
                 _width: 200,
+                ai: 'dall-e',
+                tags: new List<string>(['@ai']),
                 data_nativeWidth: 1024,
                 data_nativeHeight: 1024,
             });
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 0a30b14b8..c672bc718 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -1,6 +1,7 @@
 import { imageUrlToBase64 } from '../../../ClientUtils';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
+import { List } from '../../../fields/List';
 import { DocCast, ImageCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { Upload } from '../../../server/SharedMediaTypes';
@@ -53,7 +54,10 @@ export class DrawingFillHandler {
                                         undefined,
                                         Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
                                             ai: 'firefly',
+                                            tags: new List<string>(['@ai']),
                                             title: newPrompt,
+                                            _data_usePath: 'alternate:hover',
+                                            data_alternates: new List<Doc>([drawing]),
                                             ai_firefly_prompt: newPrompt,
                                             _width: 500,
                                             data_nativeWidth: info.nativeWidth,
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index ca308015d..1cceabed3 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -28,6 +28,7 @@ import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from '
 import './SmartDrawHandler.scss';
 import { Upload } from '../../../server/SharedMediaTypes';
 import { PointData } from '../../../pen-gestures/GestureTypes';
+import { List } from '../../../fields/List';
 
 export interface DrawingOptions {
     text?: string;
@@ -293,15 +294,17 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     return undefined;
                 }
                 const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)?.[1];
-                const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
+                return Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                     title: input,
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
+                    tags: new List<string>(['@ai']),
+                    _width: Math.min(400, dims.width),
+                    _height: (Math.min(400, dims.width) * dims.height) / dims.width,
                     ai: 'firefly',
                     ai_firefly_seed: +(newseed ?? 0),
                     ai_firefly_prompt: input,
                 });
-                return imgDoc;
             })
             .catch(e => {
                 alert('create image failed: ' + e.toString());
@@ -568,6 +571,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         color={SettingsManager.userColor}
                     />
                     <input
+                        style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
                         aria-label="Smart Draw Input"
                         className="smartdraw-input"
                         type="text"
-- 
cgit v1.2.3-70-g09d2

