From d0cbb43ec3fa19f76a570f5e0038bfc72c9f37b9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 May 2025 11:21:53 -0400
Subject: fixed dont_center on Docs in gridview.  fixed ai view screentolocal
 in docView, and made ai view more streamlined.  got rid of ai history sidebar
 in images.   fixed imageeditor to use a mask that doesn't show the original
 conctents.  fixed gptpopup scrolling, turning off of spinner, and improved
 ability to filter Docs

---
 src/client/views/nodes/ImageBox.tsx | 96 ++++++++++---------------------------
 1 file changed, 26 insertions(+), 70 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d16baada6..bf6915570 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,6 +1,6 @@
-import { Button, Colors, EditableText, IconButton, Size, Toggle, ToggleType, Type } from '@dash/components';
+import { Button, Colors, EditableText, IconButton, NumberDropdown, Size, Toggle, ToggleType, Type } from '@dash/components';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Slider, Tooltip } from '@mui/material';
+import { Tooltip } from '@mui/material';
 import axios from 'axios';
 import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -37,7 +37,6 @@ 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 { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { StickerPalette } from '../smartdraw/StickerPalette';
 import { StyleProp } from '../StyleProp';
@@ -102,7 +101,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable private _regenInput = '';
     @observable private _canInteract = true;
     @observable private _regenerateLoading = false;
-    @observable private _prevImgs: FireflyImageData[] = StrCast(this.Document.ai_firefly_history) ? JSON.parse(StrCast(this.Document.ai_firefly_history)) : [];
 
     // Add these observable properties to the ImageBox class
     @observable private _outpaintingInProgress = false;
@@ -845,34 +843,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
     @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]));
         return (
             <div className="imageBox-aiView">
                 <div className="imageBox-aiView-regenerate">
-                    <span className="imageBox-aiView-firefly" style={{ color: SnappingManager.userColor }}>
-                        Firefly:
-                    </span>
                     <input
                         style={{ color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}
                         className="imageBox-aiView-input"
@@ -886,57 +860,39 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         <Button
                             text="Create"
                             type={Type.TERT}
+                            size={Size.XSMALL}
                             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"
-                            onClick={action(async () => {
+                            onClick={action(() => {
                                 this._regenerateLoading = true;
-                                if (this._fireflyRefStrength) {
-                                    DrawingFillHandler.drawingToImage(this.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 => {
-                                            const firstImg = newImgs[0];
-                                            if (isFireflyImageData(firstImg)) {
-                                                const url = firstImg.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: firstImg.prompt, seed: firstImg.seed, pathname: url });
-                                                this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
-                                                this.dataDoc.ai_firefly_prompt = firstImg.prompt;
-                                                this.dataDoc[this.fieldKey] = imgField;
-                                                this._regenerateLoading = false;
-                                                this._regenInput = '';
-                                            }
-                                        })
-                                    );
-                                }
+                                DrawingFillHandler.drawingToImage(this.Document, this._fireflyRefStrength, this._regenInput || StrCast(this.Document.title))?.then(action(() => (this._regenerateLoading = false)));
                             })}
                         />
                     </div>
-                </div>
-                <div className="imageBox-aiView-strength">
-                    <span className="imageBox-aiView-similarity" style={{ color: SnappingManager.userColor }}>
-                        Similarity
-                    </span>
-                    <Slider
-                        className="imageBox-aiView-slider"
-                        sx={{
-                            '& .MuiSlider-track': { color: SettingsManager.userColor },
-                            '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor },
-                            '& .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}
-                        step={1}
-                        size="small"
-                        value={this._fireflyRefStrength}
-                        onChange={action((e, val) => this._canInteract && (this._fireflyRefStrength = val as number))}
-                        valueLabelDisplay="auto"
-                    />
+                    <div>
+                        <NumberDropdown
+                            color={SnappingManager.userColor}
+                            background={SnappingManager.userBackgroundColor}
+                            numberDropdownType="slider"
+                            showPlusMinus={false}
+                            formLabel="similarity"
+                            tooltip="structure similarity of created images to current image"
+                            type={Type.PRIM}
+                            width={75}
+                            min={0}
+                            max={100}
+                            number={this._fireflyRefStrength}
+                            size={Size.XXSMALL}
+                            setNumber={undoable(
+                                action(val => this._canInteract && (this._fireflyRefStrength = val as number)),
+                                `${this.Document.title} button set from list`
+                            )}
+                            fillWidth
+                        />
+                    </div>
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From 4b8c4c9277cca69ea3341ca6ddf4c7f5befc78a0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 May 2025 15:46:06 -0400
Subject: cleaned up ai metadata on generated drawings.

---
 src/client/documents/Documents.ts                  |  5 +-
 src/client/util/DocumentManager.ts                 |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 54 ++-----------------
 src/client/views/nodes/ImageBox.tsx                |  6 +--
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |  4 +-
 src/client/views/smartdraw/DrawingFillHandler.tsx  |  6 +--
 src/client/views/smartdraw/SmartDrawHandler.scss   |  1 +
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 63 +++++++++++-----------
 src/client/views/smartdraw/StickerPalette.tsx      | 12 ++---
 9 files changed, 57 insertions(+), 96 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index bbe872a91..9b6acef7b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -519,8 +519,9 @@ export class DocumentOptions {
     card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
 
     ai?: string; // to mark items as ai generated
-    ai_firefly_seed?: number;
-    ai_firefly_prompt?: string;
+    ai_prompt_seed?: NUMt = new NumInfo('seed to GAI engine to make results deterministic');
+    ai_prompt?: STRt = new StrInfo('input prompt to GAI engine');
+    ai_generatedDocs?: List<Doc>; // list of documents generated by GAI engine
 
     /**
      * JSON‐stringified slot configuration for ScrapbookBox
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 3bae2881e..fc8f22cf3 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -349,7 +349,7 @@ export class DocumentManager {
             // bcz: should this delay be an options parameter?
             setTimeout(() => {
                 Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect);
-                const zoomableText = StrCast(targetDoc.text_html, StrCast(targetDoc.ai_firefly_prompt));
+                const zoomableText = StrCast(targetDoc.text_html, StrCast(targetDoc.ai_prompt));
                 if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && zoomableText) {
                     // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
                     contextView.setTextHtmlOverlay(zoomableText, options.effect);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 05958e6b9..bb3c59eae 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1259,15 +1259,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     /**
      * Adds the created drawing to the freeform canvas and sets the metadata.
      */
-    addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => {
-        doc.$title = opts.text;
-        doc.$width = opts.size;
-        doc.$ai_drawing_input = opts.text;
-        doc.$ai_drawing_complexity = opts.complexity;
-        doc.$ai_drawing_colored = opts.autoColor;
-        doc.$ai_drawing_size = opts.size;
-        doc.$ai_drawing_data = gptRes;
-        doc.$ai = 'gpt';
+    addDrawing = (doc: Doc, opts: DrawingOptions, x?: number, y?: number) => {
+        doc.$ai_prompt = opts.text;
         this._drawingContainer = doc;
         if (x !== undefined && y !== undefined) {
             [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(x, y);
@@ -1977,7 +1970,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 event: action(() => {
                     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
                     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
-                    !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
+                    !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, NumCast(this.layoutDoc[this.scaleFieldKey])) : SmartDrawHandler.Instance.hideRegenerate();
                 }),
                 icon: 'pen-to-square',
             });
@@ -2182,9 +2175,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._drawingFillInput = e.target.value;
-                            })}
+                            onChange={action(e => (this._drawingFillInput = e.target.value))}
                         />
                         <div className="collectionFreeFormView-aiView-strength">
                             <span className="collectionFreeFormView-aiView-similarity">Similarity</span>
@@ -2213,11 +2204,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 onClick={undoable(
                                     action(() => {
                                         this._drawingFillLoading = true;
-                                        DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(
-                                            action(() => {
-                                                this._drawingFillLoading = false;
-                                            })
-                                        );
+                                        DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(action(() => (this._drawingFillLoading = false)));
                                     }),
                                     'create image'
                                 )}
@@ -2225,37 +2212,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         </div>
                     </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._regenInput = e.target.value;
-                            })}
-                            placeholder="..under development.."
-                        />
-                        <div className="collectionFreeFormView-aiView-regenBtn">
-                            <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>
             </div>
         );
     };
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index bf6915570..b648f2adb 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -225,7 +225,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     const drag = de.complete.docDragData.draggedDocuments.lastElement();
                     const dragField = drag[Doc.LayoutDataKey(drag)];
                     const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title);
-                    const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title));
+                    const oldPrompt = StrCast(this.Document.ai_prompt, StrCast(this.Document.title));
                     const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text);
                     DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false)));
                     added = false;
@@ -735,9 +735,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div
                     className="imageBox-regenerateDropTarget"
                     ref={this._regenerateIconRef}
-                    onClick={() => DocCast(this.Document.ai_firefly_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs)!, { openLocation: OpenWhere.addRight })}
+                    onClick={() => DocCast(this.Document.ai_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_generatedDocs)!, { openLocation: OpenWhere.addRight })}
                     style={{
-                        display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none',
+                        display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_generatedDocs)) || this._regenerateLoading ? 'block' : 'none',
                         transform: `scale(${this.uiBtnScaling})`,
                         width: this._sideBtnWidth,
                         height: this._sideBtnWidth,
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 9fbae5c90..568e48edf 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -211,7 +211,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         const selView = DocumentView.Selected().lastElement();
         const selDoc = selView?.Document;
         if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutDataKey(selDoc)] instanceof ImageField)) {
-            const oldPrompt = StrCast(selDoc.ai_firefly_prompt, StrCast(selDoc.title));
+            const oldPrompt = StrCast(selDoc.ai_prompt, StrCast(selDoc.title));
             const newPrompt = oldPrompt ? `${oldPrompt} ~~~ ${imgDesc}` : imgDesc;
             return DrawingFillHandler.drawingToImage(selDoc, 100, newPrompt, selDoc)
                 .then(action(() => (this._userPrompt = '')))
@@ -767,7 +767,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     //prettier-ignore
                     switch (this._mode) {
                         case GPTPopupMode.USER_PROMPT:   return this.promptBox("ASK", this._userPrompt, this.setUserPrompt, 'Ask GPT to sort, tag, define, or filter your documents for you!');
-                        case GPTPopupMode.FIREFLY:       return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, StrCast(DocumentView.Selected().lastElement()?.Document.ai_firefly_prompt, 'Ask Firefly to generate images'));
+                        case GPTPopupMode.FIREFLY:       return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, StrCast(DocumentView.Selected().lastElement()?.Document.ai_prompt, 'Ask Firefly to generate images'));
                         case GPTPopupMode.QUIZ_RESPONSE: return this.promptBox("QUIZ", this._quizAnswer, this.setQuizAnswer, 'Describe/answer the selected document!');
                         case GPTPopupMode.GPT_MENU:      return this.menuBox();
                         case GPTPopupMode.SUMMARY:       return this.summaryBox();
diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx
index 2c69284db..f773957e7 100644
--- a/src/client/views/smartdraw/DrawingFillHandler.tsx
+++ b/src/client/views/smartdraw/DrawingFillHandler.tsx
@@ -51,19 +51,19 @@ export class DrawingFillHandler {
                             if (error.includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) {
                                 return DrawingFillHandler.authorizeDropbox();
                             }
-                            const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 });
-                            drawing.$ai_firefly_generatedDocs = genratedDocs;
+                            const genratedDocs = DocCast(drawing.ai_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 });
+                            drawing.$ai_generatedDocs = genratedDocs;
                             (res as Upload.ImageInformation[]).map(info =>
                                 Doc.AddDocToList(
                                     genratedDocs,
                                     undefined,
                                     Docs.Create.ImageDocument(info.accessPaths.agnostic.client, {
                                         ai: 'firefly',
+                                        ai_prompt: newPrompt,
                                         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,
                                         data_nativeHeight: info.nativeHeight,
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
index cca7d77c7..e80f1122b 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.scss
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -73,5 +73,6 @@
     .edit-box {
         display: flex;
         flex-direction: row;
+        color: black;
     }
 }
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index a6e221f3e..3976ec39e 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -61,7 +61,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     static Instance: SmartDrawHandler;
 
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
-    private _lastResponse: string = '';
     private _selectedDocs: Doc[] = [];
 
     @observable private _display: boolean = false;
@@ -97,7 +96,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added
         or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.)
     */
-    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => void = unimplementedFunction;
+    public AddDrawing: (doc: Doc, opts: DrawingOptions, x?: number, y?: number) => void = unimplementedFunction;
     public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction;
     /**
      * This creates the ink document that represents a drawing, so it goes through the strokes that make up the drawing,
@@ -105,7 +104,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of
      * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions.
      */
-    public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
+    public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
         const drawing: Doc[] = [];
         strokeList.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
@@ -130,7 +129,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             drawing.push(inkDoc);
         });
 
-        return MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+        const drawn = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+
+        drawn.$ai_drawing = true;
+        drawn.$ai_drawing_complexity = opts.complexity;
+        drawn.$ai_drawing_colored = opts.autoColor;
+        drawn.$ai_drawing_size = opts.size;
+        drawn.$ai_drawing_data = gptRes;
+        return drawn;
     };
 
     @action
@@ -146,15 +152,16 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * the regenerate popup show by user command.
      */
     @action
-    displayRegenerate = (x: number, y: number) => {
+    displayRegenerate = (x: number, y: number, scale: number) => {
         this._selectedDocs = [DocumentView.SelectedDocs()?.lastElement()];
         [this._pageX, this._pageY] = [x, y];
+        this._scale = scale;
         this._display = false;
         this.ShowRegenerate = true;
         this._showEditBox = false;
         const docData = this._selectedDocs[0];
-        this._lastResponse = StrCast(docData.$drawingData);
-        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 };
+        this._regenInput = StrCast(docData.$ai_prompt, StrCast(docData.title));
+        this._lastInput = { text: StrCast(docData.$ai_prompt), complexity: NumCast(docData.$ai_drawing_complexity), size: NumCast(docData.$ai_drawing_size), autoColor: BoolCast(docData.$ai_drawing_colored), x: this._pageX, y: this._pageY };
     };
 
     /**
@@ -168,9 +175,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             this._isLoading = false;
             this._showOptions = false;
             this._userInput = '';
-            this._complexity = 5;
-            this._size = 350;
-            this._autoColor = true;
             Doc.ActiveTool = InkTool.None;
         }
     };
@@ -184,7 +188,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             this.ShowRegenerate = false;
             this._isLoading = false;
             this._regenInput = '';
-            this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
         }
     };
 
@@ -208,7 +211,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._isLoading = true;
         this._canInteract = false;
         if (this.ShowRegenerate) {
-            await this.regenerate(this._selectedDocs, undefined, undefined, this._regenInput).then(action(() => (this._showEditBox = false)));
+            this._lastInput.x = X;
+            this._lastInput.y = Y;
+            await this.regenerate(this._selectedDocs).then(action(() => (this._showEditBox = false)));
         } else {
             this._showOptions = false;
             try {
@@ -234,13 +239,15 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     drawWithGPT = async (screenPt: { 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: screenPt.X, y: screenPt.Y };
+            this._lastInput = { text: input, complexity, size, autoColor, x: screenPt.X, y: screenPt.Y };
             const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
             if (res) {
                 const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, false, autoColor);
                 const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res, screenPt.X, screenPt.Y);
-                drawingDoc && this._selectedDocs.push(drawingDoc);
+                if (drawingDoc) {
+                    this.AddDrawing(drawingDoc, this._lastInput, screenPt.X, screenPt.Y);
+                    this._selectedDocs.push(drawingDoc);
+                }
                 return strokeData;
             } else {
                 console.error('GPT call failed');
@@ -255,7 +262,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     createImageWithFirefly = (input: string, seed?: number): Promise<FireflyImageData | Doc | undefined> => {
         this._lastInput.text = input;
         return SmartDrawHandler.CreateWithFirefly(input, this._imgDims, seed).then(doc => {
-            doc instanceof Doc && this.AddDrawing(doc, this._lastInput, input, this._pageX, this._pageY);
+            doc instanceof Doc && this.AddDrawing(doc, this._lastInput, this._pageX, this._pageY);
             return doc;
         });
     }; /**
@@ -301,8 +308,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     _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,
+                    ai_prompt_seed: +(newseed ?? 0),
+                    ai_prompt: input,
                 });
             })
             .catch(e => {
@@ -316,33 +323,30 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * @param doc the drawing Docs to regenerate
      */
     @action
-    regenerate = (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => {
-        if (lastInput) this._lastInput = lastInput;
-        if (lastResponse) this._lastResponse = lastResponse;
+    regenerate = (drawingDocs: Doc[], regenInput?: string, changeInPlace?: boolean) => {
         if (regenInput) this._regenInput = regenInput;
         return Promise.all(
             drawingDocs.map(async doc => {
                 switch (doc.type) {
                     case DocumentType.IMG: {
                         const func = changeInPlace ? this.recreateImageWithFirefly : this.createImageWithFirefly;
-                        const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
-                        return this._regenInput ? func(newPrompt, NumCast(doc?.ai_firefly_seed)) : func(this._lastInput.text || StrCast(doc.ai_firefly_prompt));
+                        const newPrompt = doc.ai_prompt && doc.ai_prompt !== this._regenInput ? `${doc.ai_prompt} ~~~ ${this._regenInput}` : this._regenInput;
+                        return this._regenInput ? func(newPrompt, NumCast(doc?.ai_prompt_seed)) : func(this._lastInput.text || StrCast(doc.ai_prompt));
                     }
                     case DocumentType.COL: {
                         try {
                             const res = await (async () => {
                                 if (this._regenInput) {
-                                    const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+                                    const prompt = `This is your previously generated svg code: ${doc.$ai_drawing_data} for the user input "${doc.ai_prompt}". Please regenerate it with the provided specifications.`;
                                     this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
                                     return gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
                                 }
-                                return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+                                return gptAPICall(`"${doc.$ai_prompt}", "${doc.$ai_drawing_complexity}", "${doc.$ai_drawing_size}"`, GPTCallType.DRAW, undefined, true);
                             })();
                             if (res) {
-                                const strokeData = await this.parseSvg(res, { X: this._lastInput.x ?? 0, Y: this._lastInput.y ?? 0 }, true, lastInput?.autoColor || this._autoColor);
-                                this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc);
+                                const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, true, this._autoColor);
                                 const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-                                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+                                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, this._lastInput.x, this._lastInput.y);
                             } else {
                                 console.error('GPT call failed');
                             }
@@ -363,7 +367,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
 
         if (svg) {
-            this._lastResponse = svg[0];
             const svgObject = await parse(svg[0]);
             console.log(res, svgObject);
             const svgStrokes: INode[] = svgObject.children;
@@ -664,7 +667,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             <div className="regenerate-box">
                 <IconButton
                     tooltip="Regenerate"
-                    icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+                    icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
                     color={SettingsManager.userColor}
                     onClick={() => this.handleSendClick(this._pageX, this._pageY)}
                 />
diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx
index 0e234e966..6ef3d26ad 100644
--- a/src/client/views/smartdraw/StickerPalette.tsx
+++ b/src/client/views/smartdraw/StickerPalette.tsx
@@ -9,7 +9,7 @@ import ReactLoading from 'react-loading';
 import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
 import { emptyFunction, numberRange } from '../../../Utils';
 import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
-import { ImageCast, NumCast } from '../../../fields/Types';
+import { ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
@@ -147,9 +147,9 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
         this._canInteract = false;
         Promise.all(
-            numberRange(3).map(i => {
+            numberRange(3).map(() => {
                 return this._showRegenerate
-                    ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._opts, this._gptRes[i], this._userInput)
+                    ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._userInput)
                     : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity || 0, this._opts.size || 0, !!this._opts.autoColor);
             })
         ).then(() => {
@@ -161,8 +161,8 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
     });
 
     @action
-    addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
-        this._gptRes.push(gptRes);
+    addDrawing = (drawing: Doc) => {
+        this._gptRes.push(StrCast(drawing.$ai_drawing_data));
         drawing.$freeform_fitContentsToBox = true;
         Doc.AddDocToList(this._props.Doc, 'data', drawing);
     };
@@ -176,7 +176,7 @@ export class StickerPalette extends ObservableReactComponent<StickerPaletteProps
         const cIndex = NumCast(this._props.Doc.carousel_index);
         const focusedDrawing = DocListCast(this._props.Doc.data)[cIndex];
         focusedDrawing.$title = this._opts.text?.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
-        focusedDrawing.$ai_drawing_input = this._opts.text;
+        focusedDrawing.$ai_prompt = this._opts.text;
         focusedDrawing.$ai_drawing_complexity = this._opts.complexity;
         focusedDrawing.$ai_drawing_colored = this._opts.autoColor;
         focusedDrawing.$ai_drawing_size = this._opts.size;
-- 
cgit v1.2.3-70-g09d2


From 299398cb7e21259de3bf7597995840b84e7e0590 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 9 May 2025 23:01:36 -0400
Subject: added user templates to template_user global field. fixed schemas to
 not erase field values when setting a new column name. fixed templates to
 render properly when switching by fixing contentsRef to be observable. fixed
 templates with fields in stacks to autoHeight update correctly by .  fixed
 images in templates to set their nativeDim after rendering.  fixed text views
 of descriptions to auto-updated by setting the modification date of the
 description field.

---
 src/client/documents/DocUtils.ts                   | 20 +++++++++----------
 src/client/util/DropConverter.ts                   |  4 +++-
 .../CollectionStackingViewFieldColumn.tsx          |  6 ++++--
 src/client/views/collections/CollectionSubView.tsx | 13 ++++++++++--
 .../collectionSchema/CollectionSchemaView.tsx      |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  6 +++---
 src/client/views/nodes/ImageBox.tsx                |  5 +++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  3 ++-
 src/fields/Doc.ts                                  | 23 +++++++++++++++++++---
 src/server/SharedMediaTypes.ts                     |  3 +++
 10 files changed, 60 insertions(+), 25 deletions(-)

(limited to 'src/client/views/nodes/ImageBox.tsx')

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 9135899d7..36e03daed 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -642,8 +642,15 @@ export namespace DocUtils {
         return dd;
     }
 
-    export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) {
+    export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc) {
         const proto = protoIn;
+
+        if (Upload.isTextInformation(result)) {
+            proto.text = result.rawText;
+        }
+        if (Upload.isVideoInformation(result)) {
+            proto.data_duration = result.duration;
+        }
         if (Upload.isImageInformation(result)) {
             const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth);
             const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase();
@@ -677,15 +684,8 @@ export namespace DocUtils {
         const pathname = result.accessPaths.agnostic.client;
         const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
         if (doc) {
-            const proto = Doc.GetProto(doc);
-            proto.text = result.rawText;
-            !(result instanceof Error) && DocUtils.assignImageInfo(result, proto);
-            if (Upload.isVideoInformation(result)) {
-                proto.data_duration = result.duration;
-            }
-            if (overwriteDoc) {
-                Doc.removeCurrentlyLoading(overwriteDoc);
-            }
+            DocUtils.assignUploadInfo(result, Doc.GetProto(doc));
+            overwriteDoc && Doc.removeCurrentlyLoading(overwriteDoc);
             generatedDocuments.push(doc);
         }
         return doc;
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index b6b111930..1d4779d8a 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -41,7 +41,7 @@ function makeTemplate(doc: Doc, first: boolean = true): boolean {
     if (first && !docs.length) {
         // bcz: feels hacky : if the root level document has items, it's not a field template
         isTemplate = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || isTemplate;
-    } else if (docData instanceof RichTextField || docData instanceof ImageField) {
+    } else if (docData instanceof RichTextField || docData instanceof ImageField || (docData === undefined && doc.type === DocumentType.IMG)) {
         if (!StrCast(layoutDoc.title).startsWith('-')) {
             isTemplate = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true);
         }
@@ -86,6 +86,8 @@ export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) {
     dbox.dragFactory = layoutDoc;
     dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
     dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
+    const userTemplatesDoc = DocCast(Doc.UserDoc().template_user);
+    userTemplatesDoc && Doc.AddDocToList(userTemplatesDoc, 'data', layoutDoc);
     return dbox;
 }
 
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 994669734..345f60e75 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -71,6 +71,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     }
 
     _ele: HTMLElement | null = null;
+    _eleMasonrySingle = React.createRef<HTMLDivElement>();
 
     protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => {
         const dragData = de.complete.docDragData;
@@ -92,13 +93,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
     createColumnDropRef = (ele: HTMLDivElement | null) => {
         this.dropDisposer?.();
         if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this));
-        else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1);
+        else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1);
         this._ele = ele;
     };
 
     @action
     componentDidMount() {
-        this._ele && this.props.refList.push(this._ele);
+        this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current);
         this._disposers.collapser = reaction(
             () => this._props.headingObject?.collapsed,
             collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore
@@ -363,6 +364,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent<
                         }}>
                         <div
                             key={`${heading}-stack`}
+                            ref={this._eleMasonrySingle}
                             className="collectionStackingView-masonrySingle"
                             style={{
                                 padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e79d0a76d..4ac755b9d 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -561,7 +561,16 @@ export function CollectionSubView<X>() {
                             }
                             const loading = Docs.Create.LoadingDocument(file, options);
                             Doc.addCurrentlyLoading(loading);
-                            DocUtils.uploadFileToDoc(file, {}, loading);
+                            DocUtils.uploadFileToDoc(file, {}, loading).then(d => {
+                                if (d && d?.type === DocumentType.IMG) {
+                                    const imgTemplate = DocListCast(DocCast(Doc.UserDoc().template_user)?.data).find(d => d.title === 'shower');
+                                    if (imgTemplate) {
+                                        d.layout_fieldKey = 'layout_shower';
+                                        d.layout_shower = imgTemplate;
+                                    }
+                                }
+                            });
+
                             return loading;
                         })
                     ))
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index c06391f35..6442385c0 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -359,7 +359,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     @action
     addNewKey = (key: string, defaultVal: FieldType | undefined) => {
         this.childDocs.forEach(doc => {
-            doc[DocData][key] = defaultVal;
+            if (doc[DocData][key] === undefined) doc[DocData][key] = defaultVal;
         });
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index bdb97d7bb..f9c21451e 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -740,7 +740,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
     }
     widgetOverlayFunc = () => (this.widgetDecorations ? this.widgetOverlay : null);
     viewingAiEditor = () => (this._props.showAIEditor && this._componentView?.componentAIView?.() !== undefined ? this.aiEditor : null);
-    _contentsRef = React.createRef<DocumentContentsView>();
+    @observable _contentsRef: DocumentContentsView | undefined = undefined;
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -756,7 +756,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewProps & Field
                     }}>
                     <DocumentContentsView
                         {...this._props}
-                        ref={this._contentsRef}
+                        ref={action((r: DocumentContentsView) => (this._contentsRef = r))}
                         layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')}
                         pointerEvents={this.contentPointerEvents}
                         setContentViewBox={this.setContentView}
@@ -1168,7 +1168,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      * @returns boolean whether sub-component Doc is in synch with the layoutDoc that this view thinks its rendering
      */
     IsInvalid = (renderDoc?: Doc): boolean => {
-        const docContents = this._docViewInternal?._contentsRef.current;
+        const docContents = this._docViewInternal?._contentsRef;
         return !(
             (!renderDoc ||
                 (docContents?.layoutDoc === renderDoc && //
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b648f2adb..f7ad5c7e2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -152,7 +152,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         this._disposers.path = reaction(
             () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }),
             ({ nativeSize, width, height }) => {
-                if (!this.layoutDoc._layout_nativeDimEditable || !height) {
+                if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) {
+                    this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content.  afterwards, remove this flag.
                     this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
                 }
             },
@@ -1048,7 +1049,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     alert('Error uploading files - possibly due to unsupported file types');
                 } else {
                     this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client);
-                    !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc);
+                    !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc);
                 }
                 disposer();
             } else {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 824ac97da..57720baae 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1224,13 +1224,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             incomingValue => {
                 if (this.EditorView && this.ApplyingChange !== this.fieldKey) {
                     if (incomingValue?.data) {
-                        const updatedState = JSON.parse(incomingValue.data.Data);
+                        const updatedState = JSON.parse(incomingValue.data.Data.replace(/\n/g, ''));
                         if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) {
                             this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState));
                             this.tryUpdateScrollHeight();
                         }
                     } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) {
                         selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? '')));
+                        this.tryUpdateScrollHeight();
                     }
                 }
             },
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index ba94f0504..990b6606f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -25,6 +25,7 @@ import { ComputedField, ScriptField } from './ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types';
 import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
 import { gptImageLabel } from '../client/apis/gpt/GPT';
+import { DateField } from './DateField';
 
 export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>;
 export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>;
@@ -1109,6 +1110,10 @@ export namespace Doc {
             Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
             Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue);
         }
+        if (templateField.type === DocumentType.IMG) {
+            // bcz: should be a better way .. but, if the image is a template, then we can't expect to know the aspect ratio. When the image is replaced by data and rendered, we want to recomputed the native dimensions.
+            templateField[DocData].layout_resetNativeDim = true;
+        }
         // get the layout string that the template uses to specify its layout
         const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout]));
 
@@ -1184,13 +1189,15 @@ export namespace Doc {
         return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
     }
     export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
-        return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0));
+        // if this is a field template, then don't use the doc's nativeWidth/height
+        return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
         if (!doc) return 0;
         const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width
         const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0);
-        return NumCast(doc._nativeHeight, nheight || dheight);
+        // if this is a field template, then don't use the doc's nativeWidth/height
+        return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight);
     }
 
     export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
@@ -1504,7 +1511,13 @@ export namespace Doc {
                 case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutDataKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutDataKey(tdoc)]);
                 default:               return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title);
         }}); // prettier-ignore
-        return docText(doc).then(text => (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text));
+        return docText(doc).then(
+            action(text => {
+                // set the time when the date changes.  This also allows a live textbox view to react to the update, otherwise, it wouldn't take effect until the next time the view is rerendered.
+                doc['$' + Doc.LayoutDataKey(doc) + '_description_modificationDate'] = new DateField();
+                return (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text);
+            })
+        );
     }
 
     // prettier-ignore
@@ -1820,3 +1833,7 @@ ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, ran
 ScriptingGlobals.add(function toJavascriptString(str: string) {
     return Field.toJavascriptString(str as FieldType);
 });
+// eslint-disable-next-line prefer-arrow-callback
+ScriptingGlobals.add(function getDescription(doc: Doc) {
+    return Doc.getDescription(doc);
+});
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 9aa4b120f..43a9ce963 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -19,6 +19,9 @@ export enum AudioAnnoState {
 }
 
 export namespace Upload {
+    export function isTextInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
+        return 'rawText' in uploadResponse;
+    }
     export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
         return 'nativeWidth' in uploadResponse;
     }
-- 
cgit v1.2.3-70-g09d2

