From 49b3c7cbe01f830a2b1d2c02452901fe360348d3 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 11 Jul 2024 11:50:01 -0400
Subject: updates

---
 src/client/views/smartdraw/SmartDrawHandler.tsx | 95 +++++++++++++++----------
 1 file changed, 59 insertions(+), 36 deletions(-)

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

diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index d24cc9d50..24046bb83 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -7,15 +7,16 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
 import { Button, IconButton } from 'browndash-components';
 import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
-// import './ImageLabelHandler.scss';
 import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
-import { InkData } from '../../../fields/InkField';
+import { InkData, InkTool } from '../../../fields/InkField';
 import { SVGToBezier } from '../../util/bezierFit';
 const { parse } = require('svgson');
 import { Slider, Switch } from '@mui/material';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { DocumentView } from '../nodes/DocumentView';
+import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import './SmartDrawHandler.scss';
 
 export interface DrawingOptions {
     text: string;
@@ -43,7 +44,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _size: number = 200;
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
-    private _addFunc: (e: React.PointerEvent<Element>, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
+    @observable private _canInteract: boolean = true;
+    private _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
     private _deleteFunc: (doc?: Doc) => void = () => {};
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 300, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
@@ -57,12 +59,12 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
 
     @action
     setUserInput = (input: string) => {
-        this._userInput = input;
+        if (this._canInteract) this._userInput = input;
     };
 
     @action
     setRegenInput = (input: string) => {
-        this._regenInput = input;
+        if (this._canInteract) this._regenInput = input;
     };
 
     @action
@@ -72,21 +74,21 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
 
     @action
     setComplexity = (val: number) => {
-        this._complexity = val;
+        if (this._canInteract) this._complexity = val;
     };
 
     @action
     setSize = (val: number) => {
-        this._size = val;
+        if (this._canInteract) this._size = val;
     };
 
     @action
     setAutoColor = () => {
-        this._autoColor = !this._autoColor;
+        if (this._canInteract) this._autoColor = !this._autoColor;
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+    displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
@@ -95,16 +97,18 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displayRegenerate = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+    displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
         this._selectedDoc = DocumentView.SelectedDocs().lastElement();
         const docData = this._selectedDoc[DocData];
         this._addFunc = addFunc;
         this._deleteFunc = deleteFunc;
         this._pageX = x;
         this._pageY = y;
+        this._display = false;
         this._showRegenerate = true;
-        this._lastResponse = docData.drawingData as string;
-        this._lastInput = { text: docData.drawingInput as string, complexity: docData.drawingComplexity as number, size: docData.drawingSize as number, autoColor: docData.drawingColored as boolean, x: this._pageX, y: this._pageY };
+        this._showEditBox = false;
+        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 };
     };
 
     @action
@@ -117,7 +121,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         this._complexity = 5;
         this._size = 300;
         this._autoColor = true;
-        // this._regenInput = ''
+        Doc.ActiveTool = InkTool.None;
     };
 
     @action
@@ -127,12 +131,25 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         this._regenInput = '';
     };
 
+    @action
+    handleKeyPress = async (event: React.KeyboardEvent) => {
+        if (event.key === 'Enter') {
+            if (this._showRegenerate) {
+                this.regenerate();
+            } else {
+                await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput);
+                this._userInput = '';
+            }
+        }
+    };
+
     _errorOccurredOnce = false;
     @action
-    drawWithGPT = async (e: React.PointerEvent<Element>, input: string) => {
+    drawWithGPT = async (startPt: { X: number; Y: number }, input: string) => {
         if (input === '') return;
-        this._lastInput = { text: input, complexity: this._complexity, size: this._size, autoColor: this._autoColor, x: e.clientX, y: e.clientY };
+        this._lastInput = { text: input, complexity: this._complexity, size: this._size, autoColor: this._autoColor, x: startPt.X, y: startPt.Y };
         this._isLoading = true;
+        this._canInteract = false;
         this._showOptions = false;
         try {
             const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW, undefined, true);
@@ -141,20 +158,22 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log(res);
-            await this.parseResponse(e, res, { X: e.clientX, Y: e.clientY }, false);
+            const strokeData = await this.parseResponse(res, startPt, false);
             this.hideSmartDrawHandler();
             this._showRegenerate = true;
             this._errorOccurredOnce = false;
+            this._isLoading = false;
+            this._canInteract = true;
+            return strokeData;
         } catch (err) {
             if (this._errorOccurredOnce) {
                 console.error('GPT call failed', err);
                 this._errorOccurredOnce = false;
             } else {
                 this._errorOccurredOnce = true;
-                this.drawWithGPT(e, input);
+                this.drawWithGPT(startPt, input);
             }
         }
-        this._isLoading = false;
     };
 
     @action
@@ -163,14 +182,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    regenerate = async (e: React.PointerEvent<Element>) => {
+    regenerate = async () => {
         this._isLoading = true;
+        this._canInteract = false;
         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}`;
+                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);
             }
@@ -179,25 +199,26 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log(res);
-            this.parseResponse(e, res, { X: this._lastInput.x, Y: this._lastInput.y }, true);
+            this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true);
         } catch (err) {
             console.error('GPT call failed', err);
         }
         this._isLoading = false;
+        this._canInteract = true;
         this._regenInput = '';
         this._showEditBox = false;
     };
 
     @action
-    parseResponse = async (e: React.PointerEvent<Element>, res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => {
+    parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => {
         const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+        console.log(svg);
         console.log('start point is', startPoint);
         if (svg) {
             this._lastResponse = svg[0];
             const svgObject = await parse(svg[0]);
             const svgStrokes: any = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
-            console.log('autocolor is', this._autoColor);
             svgStrokes.forEach((child: any) => {
                 const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
                 strokeData.push([
@@ -210,10 +231,11 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             });
             if (regenerate) {
                 this._deleteFunc(this._selectedDoc);
-                this._addFunc(e, strokeData, this._lastInput, svg[0], this._selectedDoc);
+                this._addFunc(strokeData, this._lastInput, svg[0], this._selectedDoc);
             } else {
-                this._addFunc(e, strokeData, this._lastInput, svg[0]);
+                this._addFunc(strokeData, this._lastInput, svg[0]);
             }
+            return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
         }
     };
 
@@ -222,7 +244,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             return (
                 <div
                     id="label-handler"
-                    className="contextMenu-cont"
+                    className="smart-draw-handler"
                     style={{
                         display: this._display ? '' : 'none',
                         left: this._pageX,
@@ -242,8 +264,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             style={{ width: '19px' }}
                         />
                         <input
-                            aria-label="label-input"
-                            id="new-label"
+                            aria-label="Smart Draw Input"
+                            className="smartdraw-input"
+                            id="smartdraw-input"
                             type="text"
                             style={{ color: 'black' }}
                             value={this._userInput}
@@ -251,7 +274,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                 this.setUserInput(e.target.value);
                             }}
                             placeholder="Enter item to draw"
+                            onKeyDown={this.handleKeyPress}
                         />
+                        <IconButton tooltip="Advanced Options" icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} color={SettingsManager.userColor} style={{ width: '14px' }} onClick={this.setShowOptions} />
                         <Button
                             style={{ alignSelf: 'flex-end' }}
                             text="Send"
@@ -259,7 +284,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             iconPlacement="right"
                             color={SettingsManager.userColor}
                             onClick={e => {
-                                this.drawWithGPT(e as React.PointerEvent<Element>, this._userInput);
+                                this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput);
                             }}
                         />
                     </div>
@@ -349,7 +374,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             return (
                 <div
                     id="smartdraw-options-menu"
-                    className="contextMenu-cont"
+                    className="smart-draw-handler"
                     style={{
                         left: this._pageX,
                         ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
@@ -365,9 +390,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             tooltip="Regenerate"
                             icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
                             color={SettingsManager.userColor}
-                            onClick={e => {
-                                this.regenerate(e as React.PointerEvent<Element>);
-                            }}
+                            onClick={this.regenerate}
                         />
                         <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
                         {this._showEditBox && (
@@ -378,6 +401,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                 }}>
                                 <input
                                     aria-label="Edit instructions input"
+                                    className="smartdraw-input"
                                     id="regen-input"
                                     type="text"
                                     style={{ color: 'black' }}
@@ -385,6 +409,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                     onChange={e => {
                                         this.setRegenInput(e.target.value);
                                     }}
+                                    onKeyDown={this.handleKeyPress}
                                     placeholder="Edit instructions"
                                 />
                                 <Button
@@ -393,9 +418,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                     icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                     iconPlacement="right"
                                     color={SettingsManager.userColor}
-                                    onClick={e => {
-                                        this.regenerate(e as React.PointerEvent<Element>);
-                                    }}
+                                    onClick={this.regenerate}
                                 />
                             </div>
                         )}
-- 
cgit v1.2.3-70-g09d2


From 3e1ef3d0b5445065ab3f44ffc17bbb04efaa8c8a Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 23 Jul 2024 12:07:53 -0400
Subject: merging w/ zach's branch

---
 src/.DS_Store                                      | Bin 10244 -> 10244 bytes
 src/client/documents/DocumentTypes.ts              |   1 +
 src/client/util/CurrentUserUtils.ts                |   1 +
 src/client/views/DocumentButtonBar.tsx             |  12 +-
 src/client/views/LightboxView.tsx                  |  13 +-
 src/client/views/MainView.tsx                      |   2 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  50 +--
 src/client/views/smartdraw/AnnotationPalette.tsx   | 370 ++++++++++++++-------
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  97 +++---
 9 files changed, 360 insertions(+), 186 deletions(-)

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

diff --git a/src/.DS_Store b/src/.DS_Store
index 75cff7b55..d7a0cd9d4 100644
Binary files a/src/.DS_Store and b/src/.DS_Store differ
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8f95068db..03ae2efb7 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -26,6 +26,7 @@ export enum DocumentType {
     FUNCPLOT = 'funcplot', // function plotter
     MAP = 'map',
     DATAVIZ = 'dataviz',
+    ANNOPALETTE = 'annopalette',
     LOADING = 'loading',
     SIMULATION = 'simulation', // physics simulation
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 24a5de42b..fa0cb920d 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -441,6 +441,7 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc,      clickFactory: DocCast(doc.emptySlide),      openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader),     openFactoryAsDelegate: true,            funcs: { hidden: "IsNoviceMode()"} },
+            { toolTip: "Tap or drag to create an annotation palette",title: "Annotation Palette", icon: "palette", dragFactory: doc.emptyAnnoPalette as Doc, clickFactory: DocCast(doc.emptyAnnoPalette)},
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as any,   openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
            // { toolTip: "Toggle an UndoStack",                     title: "undostacker", icon: "calculator",                                      clickFactory: "<UndoStack />" as any,         openFactoryLocation: OpenWhere.overlay},
           ].map(tuple => (
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 177546fdc..eb0b00472 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -244,12 +244,14 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     }
 
     @observable _annoSaved: boolean = false;
-    @action
-    saveAnno = (targetDoc: Doc) => {
-        targetDoc.savedAsAnno = true;
+
+    @undoBatch
+    saveAnno = action((targetDoc: Doc) => {
+        // targetDoc.savedAsAnno = true;
         this._annoSaved = true;
-        AnnotationPalette.Instance.saveAnno(this.view0, targetDoc);
-    };
+        AnnotationPalette.Instance.addToPalette(targetDoc);
+    });
+
     @computed
     get saveAnnoButton() {
         const targetDoc = this.view0?.Document;
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index dcd5a61c7..4fcb7ec9c 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -24,6 +24,7 @@ import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { OverlayView } from './OverlayView';
 import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { DocData } from '../../fields/DocSymbols';
 
 interface LightboxViewProps {
     PanelWidth: number;
@@ -41,7 +42,14 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
      * @param view
      * @returns true if a DocumentView is descendant of the lightbox view
      */
-    public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore
+    public static Contains(view?: DocumentView) {
+        return true;
+    }
+    // return (
+    // (view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView)) ||
+    // view?.Document === AnnotationPalette.Instance.FreeformCanvas ||
+    // view?.Document.embedContainer === AnnotationPalette.Instance.DrawingCarousel
+    // ); } // prettier-ignore
     public static LightboxDoc = () => LightboxView.Instance?._doc;
     // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
@@ -210,6 +218,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     togglePalette = () => {
         this._showPalette = !this._showPalette;
         AnnotationPalette.Instance.displayPalette(this._showPalette);
+        if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
     };
     togglePen = () => {
         Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
@@ -327,7 +336,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                 )}
                 <LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} />
                 {toggleBtn('lightboxView-navBtn', 'toggle reading view', this._doc?._layout_fitWidth, 'book-open', 'book', this.toggleFitWidth)}
-                {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-download', '', this.downloadDoc)}
+                {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)}
                 {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)}
                 {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
                 {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2e7cb1102..d9a8730e4 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -342,6 +342,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faTerminal,
                 fa.faToggleOn,
                 fa.faFile,
+                fa.faFileExport,
                 fa.faLocationArrow,
                 fa.faSearch,
                 fa.faFileDownload,
@@ -485,6 +486,7 @@ export class MainView extends ObservableReactComponent<{}> {
                 fa.faAlignJustify,
                 fa.faCheckSquare,
                 fa.faSquarePlus,
+                fa.faReply,
                 fa.faListUl,
                 fa.faWindowMinimize,
                 fa.faWindowRestore,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3cd1e99ef..d0f65866b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -57,6 +57,7 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
+import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
 
 @observer
 class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -662,18 +663,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             _width: B.width + inkWidth,
                             _height: B.height + inkWidth,
                             stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                            inkWidth,
-                            ActiveInkColor(),
-                            ActiveInkBezierApprox(),
-                            ActiveFillColor(),
-                            ActiveArrowStart(),
-                            ActiveArrowEnd(),
-                            ActiveDash(),
-                            ActiveIsInkMask()
-                        );
-                    });
-                    newStrokes && this.addDocument?.(newStrokes);
-                    // setTimeout(() => this._eraserLock--);
+                                inkWidth,
+                                ActiveInkColor(),
+                                ActiveInkBezierApprox(),
+                                ActiveFillColor(),
+                                ActiveArrowStart(),
+                                ActiveArrowEnd(),
+                                ActiveDash(),
+                                ActiveIsInkMask()
+                            );
+                        });
+                        newStrokes && this.addDocument?.(newStrokes);
+                        // setTimeout(() => this._eraserLock--);
+                    }
                 }
             });
         }
@@ -1284,10 +1286,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @undoBatch
     createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
         this._drawing = [];
-        this._drawingContainer = undefined;
+        const xf = this.screenToFreeformContentsXf;
+        // this._drawingContainer = undefined;
         strokeData.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
-            const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+            const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
             const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
             const inkDoc = Docs.Create.InkDocument(
                 stroke[0],
@@ -1299,16 +1302,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
                 inkWidth,
                 stroke[1],
-                undefined,
-                stroke[2] === 'none' ? undefined : stroke[2]
+                ActiveInkBezierApprox(),
+                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+                ActiveArrowStart(),
+                ActiveArrowEnd(),
+                ActiveDash(),
+                ActiveIsInkMask()
             );
             this._drawing.push(inkDoc);
             this.addDocument(inkDoc);
         });
-        const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+        const collection = containerDoc || this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
         if (collection) {
             const docData = collection[DocData];
-            docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[0] || opts.text;
+            docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
             docData.drawingInput = opts.text;
             docData.drawingComplexity = opts.complexity;
             docData.drawingColored = opts.autoColor;
@@ -1325,10 +1332,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const docData = DocCast(doc[DocData]);
             const children = DocListCast(docData.data);
             this._props.removeDocument?.(children);
-            this._props.removeDocument?.(doc);
+            // this._props.removeDocument?.(doc);
         } else {
             this._props.removeDocument?.(this._drawing);
-            if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
+            // if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
         }
     };
 
@@ -2038,8 +2045,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         optionItems.push({
             description: 'Show Drawing Editor',
             event: action(() => {
-                this._showDrawingEditor = !this._showDrawingEditor;
-                this._showDrawingEditor ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+                !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
             }),
             icon: 'pen-to-square',
         });
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 10e88e91e..c8ce9e653 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -1,49 +1,49 @@
+import { faLaptopHouse } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { flexibleCompare } from '@fullcalendar/core/internal';
 import { Slider, Switch } from '@mui/material';
 import { Button, IconButton } from 'browndash-components';
-import { data } from 'jquery';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
 import { returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
-import { ActiveInkWidth, Doc, StrListCast } from '../../../fields/Doc';
+import { ActiveInkWidth, Doc, DocListCast, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkData, InkField } from '../../../fields/InkField';
-import { BoolCast, DocCast } from '../../../fields/Types';
-import { emptyFunction } from '../../../Utils';
+import { BoolCast, DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
-import { CollectionViewType } from '../../documents/DocumentTypes';
-import { DragManager } from '../../util/DragManager';
-import { convertDropDataToButtons, makeUserTemplateButton } from '../../util/DropConverter';
+import { makeUserTemplateButton } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
-import { MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm';
-import { CollectionGridView } from '../collections/collectionGrid';
-import { CollectionStackingView } from '../collections/CollectionStackingView';
-import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-import { FieldViewProps } from '../nodes/FieldView';
+import { undoable, undoBatch } from '../../util/UndoManager';
+import { CollectionFreeFormView, MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { FieldView } from '../nodes/FieldView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './AnnotationPalette.scss';
-import { SmartDrawHandler } from './SmartDrawHandler';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { ImageField } from '../../../fields/URLField';
+import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView';
 
 @observer
 export class AnnotationPalette extends ObservableReactComponent<{}> {
     static Instance: AnnotationPalette;
-    @observable private _savedDrawings: Doc[] = [];
-    // @observable private _marqueeViewRef = React.createRef<MarqueeView>();
     @observable private _display: boolean = false;
     @observable private _paletteMode: 'create' | 'view' = 'view';
     @observable private _userInput: string = '';
-    @observable private _showDrawing: boolean = false;
-    @observable private _drawing: Doc | undefined = undefined;
     @observable private _isLoading: boolean = false;
-    @observable private _detail: number = 5;
-    @observable private _size: number = 350;
     @observable private _canInteract: boolean = true;
+    @observable private _showRegenerate: boolean = false;
+    @observable private _freeFormCanvas = Docs.Create.FreeformDocument([], {});
+    @observable private _drawingCarousel = Docs.Create.CarouselDocument([], {});
+    @observable private _drawings: Doc[] = [];
+    private _drawing: Doc[] = [];
+    @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+    private _gptRes: string[] = [];
 
     constructor(props: any) {
         super(props);
@@ -51,8 +51,40 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
         AnnotationPalette.Instance = this;
     }
 
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(AnnotationPalette, fieldKey);
+    }
+
+    public get FreeformCanvas() {
+        return this._freeFormCanvas;
+    }
+
+    public get DrawingCarousel() {
+        return this._drawingCarousel;
+    }
+
+    // componentDidUpdate(prevProps: Readonly<{}>) {
+    //     const docView = DocumentView.getDocumentView(this._freeFormCanvas);
+    //     const componentView = docView?.ComponentView as CollectionFreeFormView;
+    //     if (componentView) {
+    //         componentView.fitContentOnce();
+    //     }
+    //     this._freeFormCanvas._freeform_fitContentsToBox = false;
+    // }
+
     return170 = () => 170;
 
+    @action
+    handleKeyPress = async (event: React.KeyboardEvent) => {
+        if (event.key === 'Enter') {
+            // if (this._showRegenerate) {
+            //     this.regenerate();
+            // } else {
+            await this.generateDrawing();
+            // }
+        }
+    };
+
     @action
     setPaletteMode = (mode: 'create' | 'view') => {
         this._paletteMode = mode;
@@ -60,31 +92,55 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
 
     @action
     setUserInput = (input: string) => {
-        this._userInput = input;
+        if (!this._isLoading) this._userInput = input;
     };
 
     @action
     setDetail = (detail: number) => {
-        this._detail = detail;
+        if (this._canInteract) this._opts.complexity = detail;
+    };
+
+    @action
+    setColor = (autoColor: boolean) => {
+        if (this._canInteract) this._opts.autoColor = autoColor;
     };
 
     @action
     setSize = (size: number) => {
-        this._size = size;
+        if (this._canInteract) this._opts.size = size;
     };
 
-    saveAnno = async (docView: DocumentView | undefined, doc: Doc) => {
-        const dragData = new DragManager.DocumentDragData([doc]);
-        // convertDropDataToButtons(dragData);
-        const clone = await Doc.MakeClone(doc);
-        clone.clone.title = doc.title;
-        const templateBtn = makeUserTemplateButton(clone.clone);
+    @action
+    resetPalette = (changePaletteMode: boolean) => {
+        if (changePaletteMode) this.setPaletteMode('view');
+        this.setUserInput('');
+        this.setDetail(5);
+        this.setColor(true);
+        this.setSize(200);
+        this._freeFormCanvas = Docs.Create.FreeformDocument([], {});
+        this._drawingCarousel = Docs.Create.CarouselDocument([], {});
+        this._showRegenerate = false;
+        this._canInteract = true;
+        this._drawing = [];
+        this._drawings = [];
+        this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+        this._gptRes = [];
+    };
 
-        // const cloneData: Doc = DocCast(clone.clone[DocData]);
-        // cloneData.dragFactory = doc;
-        Doc.AddDocToList(Doc.MyAnnos, 'data', templateBtn);
-        // const collection = this._marqueeViewRef.current?.collection(undefined, false, this._savedDrawings);
-        // if (docView) docView.ComponentView?.removeDocument?.(doc);
+    addToPalette = async (doc: Doc) => {
+        if (!doc.savedAsAnno) {
+            const clone = await Doc.MakeClone(doc);
+            clone.clone.title = doc.title;
+            const image = await this.getIcon(doc);
+            if (image) {
+                const imageDoc = Docs.Create.ImageDocument(image);
+                Doc.AddDocToList(Doc.MyAnnos, 'data', imageDoc);
+            }
+            doc.savedAsAnno = true;
+            // const templateBtn = makeUserTemplateButton(clone.clone);
+            // Doc.AddDocToList(Doc.MyAnnos, 'data', templateBtn);
+            // this.resetPalette(true);
+        }
     };
 
     @action
@@ -92,78 +148,136 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
         this._display = display;
     };
 
-    @action
-    generateDrawing = async () => {
+    @undoBatch
+    generateDrawing = action(async () => {
         this._isLoading = true;
-        try {
-            const drawingRes = await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput);
-            const opts = drawingRes?.lastInput;
-            const drawing: Doc[] = [];
-            drawingRes?.data.forEach((stroke: [InkData, string, string]) => {
-                const bounds = InkField.getBounds(stroke[0]);
-                // const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
-                const inkWidth = ActiveInkWidth();
-                const inkDoc = Docs.Create.InkDocument(
-                    stroke[0],
-                    { title: 'stroke',
-                    // x: B.x - inkWidth / 2,
-                    // y: B.y - inkWidth / 2,
-                    // _width: B.width + inkWidth,
-                    // _height: B.height + inkWidth,
-                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                    inkWidth,
-                    stroke[1],
-                    undefined,
-                    stroke[2] === 'none' ? undefined : stroke[2]
-                );
-                drawing.push(inkDoc);
-            });
-            const collection = MarqueeOptionsMenu.Instance.createCollection(undefined, true, drawing);
-            if (collection) {
-                const docData = collection[DocData];
-                docData.title = opts?.text;
-                docData.drawingInput = opts?.text;
-                docData.drawingComplexity = opts?.complexity;
-                docData.drawingColored = opts?.autoColor;
-                docData.drawingSize = opts?.size;
-                docData.drawingData = drawingRes?.lastRes;
-                this._drawing = collection;
+        this._drawings = [];
+        this._drawing = [];
+        for (var i = 0; i < 3; i++) {
+            try {
+                SmartDrawHandler.Instance._addFunc = this.createDrawing;
+                this._canInteract = false;
+                if (this._showRegenerate) {
+                    SmartDrawHandler.Instance._deleteFunc = unimplementedFunction;
+                    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');
             }
-            this._showDrawing = true;
-        } catch (e) {
-            console.log('Error generating drawing');
         }
+        this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
+        this._userInput = '';
         this._isLoading = false;
+        this._showRegenerate = true;
+    });
+
+    @action
+    createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+        this._opts = opts;
+        this._gptRes.push(gptRes);
+        this._drawing = [];
+        // const childDocs = DocListCast(this._drawing1[DocData].data);
+        strokeList.forEach((stroke: [InkData, string, string]) => {
+            const bounds = InkField.getBounds(stroke[0]);
+            const inkWidth = ActiveInkWidth();
+            const inkDoc = Docs.Create.InkDocument(
+                stroke[0],
+                { title: 'stroke',
+                    x: bounds.left - inkWidth / 2,
+                    y: bounds.top - inkWidth / 2,
+                    _width: bounds.width + inkWidth,
+                    _height: bounds.height + inkWidth,
+                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                inkWidth,
+                stroke[1],
+                ActiveInkBezierApprox(),
+                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+                ActiveArrowStart(),
+                ActiveArrowEnd(),
+                ActiveDash(),
+                ActiveIsInkMask()
+            );
+            this._drawing.push(inkDoc);
+            // childDocs.push(inkDoc);
+        });
+
+        const cv = DocumentView.getDocumentView(this._freeFormCanvas)?.ComponentView as CollectionFreeFormView;
+        const collection = cv._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+        if (collection) {
+            this._drawings.push(collection);
+            cv.fitContentOnce();
+        }
+        this._drawingCarousel = Docs.Create.CarouselDocument(this._drawings, { childLayoutFitWidth: true, _layout_fitWidth: true, _freeform_fitContentsToBox: true });
+        this._freeFormCanvas = Docs.Create.FreeformDocument(this._drawing, { _freeform_fitContentsToBox: true });
     };
 
-    // @computed get drawingCreator() {
-    //     return (
-    //         MarqueeOptionsMenu.Instance.createCollection(undefined, true, this._drawing);
-    //     );
-    // }
-    // return Docs.Create.FreeformDocument([], {});
-    // Docs.Create.
-    // return (
-    //     <DocumentView
-    //         Document={doc}
-    //         addDocument={undefined}
-    //         addDocTab={DocumentViewInternal.addDocTabFunc}
-    //         pinToPres={DocumentView.PinDoc}
-    //         containerViewPath={returnEmptyDoclist}
-    //         styleProvider={DefaultStyleProvider}
-    //         removeDocument={returnFalse}
-    //         ScreenToLocalTransform={Transform.Identity}
-    //         PanelWidth={this.return170}
-    //         PanelHeight={this.return170}
-    //         renderDepth={0}
-    //         isContentActive={returnTrue}
-    //         focus={emptyFunction}
-    //         whenChildContentsActiveChanged={emptyFunction}
-    //         childFilters={returnEmptyFilter}
-    //         childFiltersByRanges={returnEmptyFilter}
-    //         searchFilterDocs={returnEmptyDoclist}
-    //     />
-    // );
+    saveDrawing = async () => {
+        // const cv = DocumentView.getDocumentView(this._freeFormCanvas)?.ComponentView as CollectionFreeFormView;
+        // if (cv) {
+        // const collection = cv._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+        const cIndex: number = this._drawingCarousel.carousel_index as number;
+        const focusedDrawing = this._drawings[cIndex];
+
+        const docData = focusedDrawing[DocData];
+        docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+        docData.drawingInput = this._opts.text;
+        docData.drawingComplexity = this._opts.complexity;
+        docData.drawingColored = this._opts.autoColor;
+        docData.drawingSize = this._opts.size;
+        docData.drawingData = this._gptRes[cIndex];
+        docData.width = this._opts.size;
+        // const image = await this.getIcon(collection);
+        await this.addToPalette(focusedDrawing);
+
+        // if (collection) {
+        //     const docData = collection[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;
+        //     docData.width = this._opts.size;
+        //     // const image = await this.getIcon(collection);
+        //     await this.addToPalette(collection);
+        // }
+        // }
+    };
+
+    async getIcon(group: Doc) {
+        const docView = DocumentView.getDocumentView(group);
+        if (docView) {
+            docView.ComponentView?.updateIcon?.();
+            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+        }
+        return undefined;
+    }
+
+    @computed get drawingCreator() {
+        return (
+            <DocumentView
+                Document={this._freeFormCanvas}
+                addDocument={undefined}
+                addDocTab={DocumentViewInternal.addDocTabFunc}
+                pinToPres={DocumentView.PinDoc}
+                containerViewPath={returnEmptyDoclist}
+                styleProvider={DefaultStyleProvider}
+                removeDocument={returnFalse}
+                ScreenToLocalTransform={Transform.Identity}
+                PanelWidth={this.return170}
+                PanelHeight={this.return170}
+                renderDepth={1}
+                isContentActive={returnTrue}
+                focus={emptyFunction}
+                whenChildContentsActiveChanged={emptyFunction}
+                childFilters={returnEmptyFilter}
+                childFiltersByRanges={returnEmptyFilter}
+                searchFilterDocs={returnEmptyDoclist}
+            />
+        );
+    }
 
     render() {
         return !this._display ? null : (
@@ -220,13 +334,13 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                 onChange={e => {
                                     this.setUserInput(e.target.value);
                                 }}
-                                placeholder="Enter item to draw"
-                                // onKeyDown={this.handleKeyPress}
+                                placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
+                                onKeyDown={this.handleKeyPress}
                             />
                             <Button
                                 style={{ alignSelf: 'flex-end' }}
-                                // text="Send"
-                                icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
+                                icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 color={SettingsManager.userColor}
                                 onClick={this.generateDrawing}
@@ -245,8 +359,9 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                         },
                                     }}
                                     defaultChecked={true}
+                                    value={this._opts.autoColor}
                                     size="small"
-                                    // onChange={this.setAutoColor}
+                                    onChange={() => this.setColor(!this._opts.autoColor)}
                                 />
                             </div>
                             <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
@@ -271,7 +386,7 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                     max={10}
                                     step={1}
                                     size="small"
-                                    value={this._detail}
+                                    value={this._opts.complexity}
                                     onChange={(e, val) => {
                                         this.setDetail(val as number);
                                     }}
@@ -297,10 +412,10 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                     }}
                                     style={{ width: '80%' }}
                                     min={50}
-                                    max={700}
+                                    max={500}
                                     step={10}
                                     size="small"
-                                    value={this._size}
+                                    value={this._opts.size}
                                     onChange={(e, val) => {
                                         this.setSize(val as number);
                                     }}
@@ -308,9 +423,9 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                 />
                             </div>
                         </div>
-                        {this._drawing !== undefined && (
+                        <div style={{ display: 'none' }}>
                             <DocumentView
-                                Document={this._drawing}
+                                Document={this._freeFormCanvas}
                                 addDocument={undefined}
                                 addDocTab={DocumentViewInternal.addDocTabFunc}
                                 pinToPres={DocumentView.PinDoc}
@@ -320,7 +435,7 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                 ScreenToLocalTransform={Transform.Identity}
                                 PanelWidth={this.return170}
                                 PanelHeight={this.return170}
-                                renderDepth={0}
+                                renderDepth={1}
                                 isContentActive={returnTrue}
                                 focus={emptyFunction}
                                 whenChildContentsActiveChanged={emptyFunction}
@@ -328,10 +443,41 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                 childFiltersByRanges={returnEmptyFilter}
                                 searchFilterDocs={returnEmptyDoclist}
                             />
-                        )}
+                        </div>
+                        <DocumentView
+                            Document={this._drawingCarousel}
+                            addDocument={undefined}
+                            addDocTab={DocumentViewInternal.addDocTabFunc}
+                            pinToPres={DocumentView.PinDoc}
+                            containerViewPath={returnEmptyDoclist}
+                            styleProvider={DefaultStyleProvider}
+                            removeDocument={returnFalse}
+                            ScreenToLocalTransform={Transform.Identity}
+                            PanelWidth={this.return170}
+                            PanelHeight={this.return170}
+                            renderDepth={1}
+                            isContentActive={returnTrue}
+                            focus={emptyFunction}
+                            whenChildContentsActiveChanged={emptyFunction}
+                            childFilters={returnEmptyFilter}
+                            childFiltersByRanges={returnEmptyFilter}
+                            searchFilterDocs={returnEmptyDoclist}
+                        />
+                        <div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
+                            <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
+                            <div style={{ display: 'flex', flexDirection: 'row' }}>
+                                <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} />
+                                <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} />
+                            </div>
+                        </div>
                     </>
                 )}
             </div>
         );
     }
 }
+
+Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
+    layout: { view: AnnotationPalette, dataField: 'data' },
+    options: { acl: '' },
+});
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 24046bb83..489f6f92b 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -17,6 +17,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { DocumentView } from '../nodes/DocumentView';
 import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
 import './SmartDrawHandler.scss';
+import { unimplementedFunction } from '../../../Utils';
 
 export interface DrawingOptions {
     text: string;
@@ -39,15 +40,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _userInput: string = '';
     @observable private _showOptions: boolean = false;
     @observable private _showEditBox: boolean = false;
-    @observable private _showRegenerate: boolean = false;
+    @observable public _showRegenerate: boolean = false;
     @observable private _complexity: number = 5;
     @observable private _size: number = 200;
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
     @observable private _canInteract: boolean = true;
-    private _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
-    private _deleteFunc: (doc?: Doc) => void = () => {};
-    private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 300, autoColor: true, x: 0, y: 0 };
+    public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
+    public _deleteFunc: (doc?: Doc) => void = () => {};
+    private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
     private _selectedDoc: Doc | undefined = undefined;
 
@@ -119,51 +120,62 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         this._showOptions = false;
         this._userInput = '';
         this._complexity = 5;
-        this._size = 300;
+        this._size = 350;
         this._autoColor = true;
         Doc.ActiveTool = InkTool.None;
+        this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     };
 
     @action
     hideRegenerate = () => {
-        this._showRegenerate = false;
-        this._isLoading = false;
-        this._regenInput = '';
+        if (!this._isLoading) {
+            this._showRegenerate = false;
+            this._isLoading = false;
+            this._regenInput = '';
+            this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
+        }
     };
 
     @action
     handleKeyPress = async (event: React.KeyboardEvent) => {
         if (event.key === 'Enter') {
-            if (this._showRegenerate) {
-                this.regenerate();
-            } else {
-                await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput);
-                this._userInput = '';
-            }
+            await this.handleSendClick();
         }
     };
 
-    _errorOccurredOnce = false;
     @action
-    drawWithGPT = async (startPt: { X: number; Y: number }, input: string) => {
-        if (input === '') return;
-        this._lastInput = { text: input, complexity: this._complexity, size: this._size, autoColor: this._autoColor, x: startPt.X, y: startPt.Y };
+    handleSendClick = async () => {
         this._isLoading = true;
         this._canInteract = false;
-        this._showOptions = false;
+        if (this._showRegenerate) {
+            await this.regenerate();
+            this._regenInput = '';
+            this._showEditBox = false;
+        } else {
+            this._showOptions = false;
+            await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+            this.hideSmartDrawHandler();
+            this._showRegenerate = true;
+        }
+        this._isLoading = false;
+        this._canInteract = true;
+    };
+
+    _errorOccurredOnce = false;
+    @action
+    drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
+        if (input === '') return;
+        this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+
         try {
-            const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW, undefined, true);
+            const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
             if (!res) {
                 console.error('GPT call failed');
                 return;
             }
             console.log(res);
-            const strokeData = await this.parseResponse(res, startPt, false);
-            this.hideSmartDrawHandler();
-            this._showRegenerate = true;
+            const strokeData = await this.parseResponse(res, startPt, false, autoColor);
             this._errorOccurredOnce = false;
-            this._isLoading = false;
-            this._canInteract = true;
             return strokeData;
         } catch (err) {
             if (this._errorOccurredOnce) {
@@ -171,7 +183,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 this._errorOccurredOnce = false;
             } else {
                 this._errorOccurredOnce = true;
-                this.drawWithGPT(startPt, input);
+                this.drawWithGPT(startPt, input, complexity, size, autoColor);
             }
         }
     };
@@ -182,9 +194,11 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    regenerate = async () => {
-        this._isLoading = true;
-        this._canInteract = false;
+    regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+        if (lastInput) this._lastInput = lastInput;
+        if (lastResponse) this._lastResponse = lastResponse;
+        if (regenInput) this._regenInput = regenInput;
+
         try {
             let res;
             if (this._regenInput !== '') {
@@ -199,21 +213,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log(res);
-            this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true);
+            this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
         } catch (err) {
             console.error('GPT call failed', err);
         }
-        this._isLoading = false;
-        this._canInteract = true;
-        this._regenInput = '';
-        this._showEditBox = false;
     };
 
     @action
-    parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => {
+    parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
         const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
-        console.log(svg);
-        console.log('start point is', startPoint);
         if (svg) {
             this._lastResponse = svg[0];
             const svgObject = await parse(svg[0]);
@@ -225,12 +233,12 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                     convertedBezier.map(point => {
                         return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
                     }),
-                    (regenerate ? this._lastInput.autoColor : this._autoColor) ? child.attributes.stroke : undefined,
-                    (regenerate ? this._lastInput.autoColor : this._autoColor) ? child.attributes.fill : undefined,
+                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : undefined,
+                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
                 ]);
             });
             if (regenerate) {
-                this._deleteFunc(this._selectedDoc);
+                if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
                 this._addFunc(strokeData, this._lastInput, svg[0], this._selectedDoc);
             } else {
                 this._addFunc(strokeData, this._lastInput, svg[0]);
@@ -283,9 +291,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                             iconPlacement="right"
                             color={SettingsManager.userColor}
-                            onClick={e => {
-                                this.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._userInput);
-                            }}
+                            onClick={this.handleSendClick}
                         />
                     </div>
                     {this._showOptions && (
@@ -303,6 +309,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                             },
                                         }}
                                         defaultChecked={true}
+                                        value={this._autoColor}
                                         size="small"
                                         onChange={this.setAutoColor}
                                     />
@@ -390,7 +397,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             tooltip="Regenerate"
                             icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
                             color={SettingsManager.userColor}
-                            onClick={this.regenerate}
+                            onClick={this.handleSendClick}
                         />
                         <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
                         {this._showEditBox && (
@@ -418,7 +425,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                                     icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                                     iconPlacement="right"
                                     color={SettingsManager.userColor}
-                                    onClick={this.regenerate}
+                                    onClick={this.handleSendClick}
                                 />
                             </div>
                         )}
-- 
cgit v1.2.3-70-g09d2


From a763c4e80cec6aef5219a8c9fcfa2ddccbc10b0a Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 24 Jul 2024 16:52:50 -0400
Subject: bug and render fixes

---
 package-lock.json                                  |  26 +--
 src/client/apis/gpt/GPT.ts                         |   2 +-
 src/client/util/CurrentUserUtils.ts                |  26 +--
 src/client/util/DropConverter.ts                   |  22 +++
 src/client/util/bezierFit.ts                       |  13 +-
 src/client/views/DocumentButtonBar.tsx             |   2 +-
 src/client/views/DocumentDecorations.tsx           |   7 +-
 src/client/views/LightboxView.tsx                  |  20 +-
 src/client/views/MainView.tsx                      |   1 -
 .../views/collections/CollectionCarousel3DView.tsx |   2 +-
 .../views/collections/CollectionCarouselView.tsx   |  11 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  10 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  13 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   | 203 +++++----------------
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   9 +-
 src/fields/Doc.ts                                  |   1 +
 16 files changed, 145 insertions(+), 223 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 663409ddb..70f53156c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13531,19 +13531,6 @@
                 "node": "^12.13.0 || ^14.15.0 || >=16.0.0"
             }
         },
-        "node_modules/browndash-components/node_modules/npm/node_modules/node-gyp/node_modules/readable-stream": {
-            "version": "3.6.2",
-            "inBundle": true,
-            "license": "MIT",
-            "dependencies": {
-                "inherits": "^2.0.3",
-                "string_decoder": "^1.1.1",
-                "util-deprecate": "^1.0.1"
-            },
-            "engines": {
-                "node": ">= 6"
-            }
-        },
         "node_modules/browndash-components/node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": {
             "version": "3.0.7",
             "inBundle": true,
@@ -13989,6 +13976,19 @@
                 "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
             }
         },
+        "node_modules/browndash-components/node_modules/npm/node_modules/readable-stream": {
+            "version": "3.6.2",
+            "inBundle": true,
+            "license": "MIT",
+            "dependencies": {
+                "inherits": "^2.0.3",
+                "string_decoder": "^1.1.1",
+                "util-deprecate": "^1.0.1"
+            },
+            "engines": {
+                "node": ">= 6"
+            }
+        },
         "node_modules/browndash-components/node_modules/npm/node_modules/retry": {
             "version": "0.12.0",
             "inBundle": true,
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index cbe2f0582..a780596fa 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -58,7 +58,7 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 1024,
         temp: 0.5,
-        prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
+        prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, polygon, and path with M, Q, C, and L so only use those.',
     },
 };
 
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 98a54d4d0..6b59828eb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -165,7 +165,7 @@ export class CurrentUserUtils {
     
     static setupAnnoPalette(doc: Doc, field="myAnnos") {
         const reqdOpts:DocumentOptions = {
-            title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, hidden: false,
+            title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", hidden: false,
             _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true,
             _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
         };
@@ -174,21 +174,14 @@ export class CurrentUserUtils {
         return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.MasonryDocument(items??[], opts), reqdOpts, DocListCast(savedAnnos?.data), reqdScripts);
     }
 
-        // static setupNoteTemplates(doc: Doc, field="template_notes") {
-        // const tempNotes = DocCast(doc[field]);
-        // const reqdTempOpts:DocumentOptions[] = [
-        //     { title: "Postit", backgroundColor: "yellow",    icon: "sticky-note", _layout_showTitle: "title", layout_borderRounding: "5px"},
-        //     { title: "Idea",   backgroundColor: "pink",      icon: "lightbulb"  , _layout_showTitle: "title"},
-        //     { title: "Topic",  backgroundColor: "lightblue", icon: "book-open"  , _layout_showTitle: "title"}];
-        // const reqdNoteList = [...reqdTempOpts.map(opts => {
-        //     const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true};
-        //     const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(fdoc => fdoc.title === opts.title): undefined;
-        //     return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts));
-        // }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))];
-
-        // const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
-        // // eslint-disable-next-line no-return-assign
-        // r
+    static setupLightboxDrawingPreviews(doc: Doc, field="myLightboxDrawings") {
+        const reqdOpts:DocumentOptions = {
+            title: "Preview", _layout_fitWidth: true,  childLayoutFitWidth: true,
+        };
+        const reqdScripts = {};
+        const drawings = DocCast(doc[field]);
+        return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.CarouselDocument(items??[], opts), reqdOpts, DocListCast(drawings?.data), reqdScripts);
+    }
 
     // setup templates for different document types when they are iconified from Document Decorations
     static setupDefaultIconTemplates(doc: Doc, field="template_icons") {
@@ -1015,6 +1008,7 @@ pie title Minerals in my tap water
         this.setupDockedButtons(doc);  // the bottom bar of font icons
         this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left
         this.setupAnnoPalette(doc);
+        this.setupLightboxDrawingPreviews(doc);
         this.setupDocTemplates(doc); // sets up the template menu of templates
         // sthis.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
         DocUtils.AssignDocField(doc, "globalScriptDatabase", () =>  Docs.Prototypes.MainScriptDocument(), {});
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 0314af06b..3e26fefad 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -86,6 +86,28 @@ export function makeUserTemplateButton(doc: Doc) {
     dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
     return dbox;
 }
+
+export function makeUserTemplateImage(doc: Doc, image: ImageField) {
+    const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+    if (layoutDoc.type !== DocumentType.FONTICON) {
+        !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
+    }
+    layoutDoc.isTemplateDoc = true;
+    const dbox = Docs.Create.ImageDocument(image, {
+        _nativeWidth: 100,
+        _nativeHeight: 100,
+        _width: 100,
+        _height: 100,
+        title: StrCast(layoutDoc.title),
+        isSystem: false,
+    });
+    dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
+    dbox.dragFactory = layoutDoc;
+    dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
+    dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
+    return dbox;
+}
+
 export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
     data?.draggedDocuments.forEach((doc, i) => {
         let dbox = doc;
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 98a6feea0..6a4577506 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -694,7 +694,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             const matches: RegExpMatchArray[] = Array.from(
                 attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
             );
-            let lastPt: Point;
+            let lastPt: Point = { X: 0, Y: 0 };
             matches.forEach(match => {
                 if (match[0].startsWith('Q')) {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
@@ -726,6 +726,17 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             }
             return coordList;
         case 'polygon':
+            const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
+            let list: Point[] = [];
+            coords.forEach(coord => {
+                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
+                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
+                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
+                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
+            });
+            const firstPts = list.splice(0, 2);
+            list = list.concat(firstPts);
+            return list;
         default:
             return [];
     }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index eb0b00472..6950bec14 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -249,7 +249,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
     saveAnno = action((targetDoc: Doc) => {
         // targetDoc.savedAsAnno = true;
         this._annoSaved = true;
-        AnnotationPalette.Instance.addToPalette(targetDoc);
+        // AnnotationPalette.addToPalette(targetDoc);
     });
 
     @computed
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a229b15db..ae4f5b98b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -229,6 +229,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                         if (iconViewDoc.activeFrame) {
                             iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
                         } else {
+                            // to mark annotations as no longer saved if they're deleted from the palette
+                            const dragFactory: Doc = DocCast(iconView.Document.dragFactory);
+                            if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) {
+                                DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
+                            }
                             iconView._props.removeDocument?.(iconView.Document);
                         }
                     });
@@ -872,4 +877,4 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
             </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 4fcb7ec9c..b0d469674 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -12,7 +12,7 @@ import { emptyFunction } from '../../Utils';
 import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
-import { Cast, NumCast, toList } from '../../fields/Types';
+import { Cast, DocCast, NumCast, toList } from '../../fields/Types';
 import { SnappingManager } from '../util/SnappingManager';
 import { Transform } from '../util/Transform';
 import { GestureOverlay } from './GestureOverlay';
@@ -43,13 +43,11 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
      * @returns true if a DocumentView is descendant of the lightbox view
      */
     public static Contains(view?: DocumentView) {
-        return true;
-    }
-    // return (
-    // (view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView)) ||
-    // view?.Document === AnnotationPalette.Instance.FreeformCanvas ||
-    // view?.Document.embedContainer === AnnotationPalette.Instance.DrawingCarousel
-    // ); } // prettier-ignore
+        return (
+            (view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView)) ||
+            (view && LightboxView.Instance?._annoPaletteView?.Contains(view))
+        ); 
+    } // prettier-ignore
     public static LightboxDoc = () => LightboxView.Instance?._doc;
     // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
@@ -69,6 +67,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     @observable private _docTarget: Opt<Doc> = undefined;
     @observable private _docView: Opt<DocumentView> = undefined;
     @observable private _showPalette: boolean = false;
+    private _annoPaletteView: AnnotationPalette | null = null;
 
     @computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore
     @computed get topBorder()  { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore
@@ -114,7 +113,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
             this._history = [];
             Doc.ActiveTool = InkTool.None;
             SnappingManager.SetExploreMode(false);
-            AnnotationPalette.Instance.displayPalette(false);
             this._showPalette = false;
         }
         DocumentView.DeselectAll();
@@ -217,8 +215,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     };
     togglePalette = () => {
         this._showPalette = !this._showPalette;
-        AnnotationPalette.Instance.displayPalette(this._showPalette);
-        if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
+        // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
     };
     togglePen = () => {
         Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
@@ -324,6 +321,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
                     </GestureOverlay>
                 </div>
 
+                {this._showPalette && <AnnotationPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />}
                 {this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length, this.previous)}
                 {this.renderNavBtn(
                     this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index fd1af7547..757fc3fe2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1119,7 +1119,6 @@ export class MainView extends ObservableReactComponent<{}> {
                 <GPTPopup key="gptpopup" />
                 <SchemaCSVPopUp key="schemacsvpopup" />
                 <GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
-                <AnnotationPalette />
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 27c85533f..241087fd2 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -78,7 +78,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 // suppressSetHeight={true}
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
-                fitWidth={undefined}
+                fitWidth={this._props.childLayoutFitWidth}
                 onDoubleClickScript={this.onChildDoubleClick}
                 renderDepth={this._props.renderDepth + 1}
                 LayoutTemplate={this._props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 2adad68e0..8b083de15 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -8,7 +8,7 @@ import * as React from 'react';
 import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { ContextMenu } from '../ContextMenu';
@@ -144,6 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         revealItems.push({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;},     icon: 'pencil',}); // prettier-ignore
         !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
     };
+    childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
     @computed get content() {
         const index = NumCast(this.layoutDoc._carousel_index);
         const curDoc = this.carouselItems?.[index];
@@ -154,10 +155,11 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <div className="collectionCarouselView-image" key="image">
                     <DocumentView
                         {...this._props}
-                        NativeWidth={returnZero}
-                        NativeHeight={returnZero}
-                        fitWidth={undefined}
+                        // NativeWidth={returnZero}
+                        // NativeHeight={returnZero}
+                        fitWidth={returnTrue}
                         setContentViewBox={undefined}
+                        containerViewPath={this.DocumentView?.().docViewPath}
                         onDoubleClickScript={this.onContentDoubleClick}
                         onClickScript={this.onContentClick}
                         isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive}
@@ -169,6 +171,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         Document={curDoc.layout}
                         TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)}
                         PanelHeight={this.panelHeight}
+                        PanelWidth={this._props.PanelWidth}
                     />
                 </div>
                 {!carouselShowsCaptions ? null : (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d0f65866b..f2b94fabc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1312,7 +1312,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this._drawing.push(inkDoc);
             this.addDocument(inkDoc);
         });
-        const collection = containerDoc || this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+        const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
         if (collection) {
             const docData = collection[DocData];
             docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
@@ -1329,14 +1329,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     removeDrawing = (doc?: Doc) => {
         this._batch = UndoManager.StartBatch('regenerateDrawing');
         if (doc) {
-            const docData = DocCast(doc[DocData]);
+            const docData = doc[DocData];
             const children = DocListCast(docData.data);
+            this._props.removeDocument?.(doc);
             this._props.removeDocument?.(children);
-            // this._props.removeDocument?.(doc);
         } else {
-            this._props.removeDocument?.(this._drawing);
-            // if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
+            if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
         }
+        this._drawing = [];
     };
 
     @action
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 5aff3ed6f..8560323c9 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -363,7 +363,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this.hideMarquee();
     });
 
-    getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>) => {
+    public static getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>, bounds: MarqueeViewBounds) => {
         const newCollection = creator
             ? creator(selected, { title: 'nested stack' })
             : ((doc: Doc) => {
@@ -375,14 +375,13 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
                   return doc;
               })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true));
         newCollection.isSystem = undefined;
-        newCollection._width = this.Bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children
-        newCollection._height = this.Bounds.height || 1;
+        newCollection._width = bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children
+        newCollection._height = bounds.height || 1;
         newCollection._dragWhenActive = makeGroup;
-        newCollection.x = this.Bounds.left;
-        newCollection.y = this.Bounds.top;
+        newCollection.x = bounds.left;
+        newCollection.y = bounds.top;
         newCollection.layout_fitWidth = true;
         selected.forEach(d => Doc.SetContainer(d, newCollection));
-        this.hideMarquee();
         return newCollection;
     });
 
@@ -419,7 +418,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             this._props.removeDocument?.(selected);
         }
 
-        const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group);
+        const newCollection = MarqueeView.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group, this.Bounds);
         newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
         newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
         newCollection._currentFrame = activeFrame;
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index c8ce9e653..83f173fcb 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -8,13 +8,13 @@ import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
 import { returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
-import { ActiveInkWidth, Doc, DocListCast, StrListCast } from '../../../fields/Doc';
+import { ActiveInkWidth, Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkData, InkField } from '../../../fields/InkField';
 import { BoolCast, DocCast, ImageCast, NumCast } from '../../../fields/Types';
 import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
-import { makeUserTemplateButton } from '../../util/DropConverter';
+import { makeUserTemplateButton, makeUserTemplateImage } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoable, undoBatch } from '../../util/UndoManager';
@@ -28,20 +28,18 @@ import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageField } from '../../../fields/URLField';
 import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView';
+import { Copy } from '../../../fields/FieldSymbols';
 
 @observer
-export class AnnotationPalette extends ObservableReactComponent<{}> {
+export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc }> {
     static Instance: AnnotationPalette;
-    @observable private _display: boolean = false;
     @observable private _paletteMode: 'create' | 'view' = 'view';
     @observable private _userInput: string = '';
     @observable private _isLoading: boolean = false;
     @observable private _canInteract: boolean = true;
     @observable private _showRegenerate: boolean = false;
-    @observable private _freeFormCanvas = Docs.Create.FreeformDocument([], {});
-    @observable private _drawingCarousel = Docs.Create.CarouselDocument([], {});
-    @observable private _drawings: Doc[] = [];
-    private _drawing: Doc[] = [];
+    @observable private _docView: DocumentView | null = null;
+    @observable private _docCarouselView: DocumentView | null = null;
     @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
     private _gptRes: string[] = [];
 
@@ -55,33 +53,16 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
         return FieldView.LayoutString(AnnotationPalette, fieldKey);
     }
 
-    public get FreeformCanvas() {
-        return this._freeFormCanvas;
-    }
-
-    public get DrawingCarousel() {
-        return this._drawingCarousel;
-    }
-
-    // componentDidUpdate(prevProps: Readonly<{}>) {
-    //     const docView = DocumentView.getDocumentView(this._freeFormCanvas);
-    //     const componentView = docView?.ComponentView as CollectionFreeFormView;
-    //     if (componentView) {
-    //         componentView.fitContentOnce();
-    //     }
-    //     this._freeFormCanvas._freeform_fitContentsToBox = false;
-    // }
+    Contains = (view: DocumentView) => {
+        return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView));
+    };
 
     return170 = () => 170;
 
     @action
     handleKeyPress = async (event: React.KeyboardEvent) => {
         if (event.key === 'Enter') {
-            // if (this._showRegenerate) {
-            //     this.regenerate();
-            // } else {
             await this.generateDrawing();
-            // }
         }
     };
 
@@ -117,42 +98,34 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
         this.setDetail(5);
         this.setColor(true);
         this.setSize(200);
-        this._freeFormCanvas = Docs.Create.FreeformDocument([], {});
-        this._drawingCarousel = Docs.Create.CarouselDocument([], {});
         this._showRegenerate = false;
         this._canInteract = true;
-        this._drawing = [];
-        this._drawings = [];
         this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
         this._gptRes = [];
     };
 
-    addToPalette = async (doc: Doc) => {
-        if (!doc.savedAsAnno) {
-            const clone = await Doc.MakeClone(doc);
-            clone.clone.title = doc.title;
-            const image = await this.getIcon(doc);
-            if (image) {
-                const imageDoc = Docs.Create.ImageDocument(image);
-                Doc.AddDocToList(Doc.MyAnnos, 'data', imageDoc);
-            }
-            doc.savedAsAnno = true;
-            // const templateBtn = makeUserTemplateButton(clone.clone);
-            // Doc.AddDocToList(Doc.MyAnnos, 'data', templateBtn);
-            // this.resetPalette(true);
-        }
+    public static addToPalette = async (doc: Doc) => {
+        // if (!doc.savedAsAnno) {
+        //     const clone = await Doc.MakeClone(doc);
+        //     clone.clone.title = doc.title;
+        //     const image = (await this.getIcon(doc))?.[Copy]();
+        //     if (image) {
+        //         const imageTemplate = makeUserTemplateImage(clone.clone, image);
+        //         Doc.AddDocToList(Doc.MyAnnos, 'data', imageTemplate);
+        //         doc.savedAsAnno = true;
+        //     }
+        //     this.resetPalette(true);
+        // }
     };
 
-    @action
-    displayPalette = (display: boolean) => {
-        this._display = display;
-    };
+    // @action
+    // displayPalette = (display: boolean) => {
+    //     this._display = display;
+    // };
 
     @undoBatch
     generateDrawing = action(async () => {
         this._isLoading = true;
-        this._drawings = [];
-        this._drawing = [];
         for (var i = 0; i < 3; i++) {
             try {
                 SmartDrawHandler.Instance._addFunc = this.createDrawing;
@@ -177,8 +150,8 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
     createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
         this._opts = opts;
         this._gptRes.push(gptRes);
-        this._drawing = [];
-        // const childDocs = DocListCast(this._drawing1[DocData].data);
+        const drawing: Doc[] = [];
+
         strokeList.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
             const inkWidth = ActiveInkWidth();
@@ -199,51 +172,27 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                 ActiveDash(),
                 ActiveIsInkMask()
             );
-            this._drawing.push(inkDoc);
-            // childDocs.push(inkDoc);
+            drawing.push(inkDoc);
         });
 
-        const cv = DocumentView.getDocumentView(this._freeFormCanvas)?.ComponentView as CollectionFreeFormView;
-        const collection = cv._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+        const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
         if (collection) {
-            this._drawings.push(collection);
-            cv.fitContentOnce();
+            Doc.AddDocToList(this._props.Document, 'data', collection);
         }
-        this._drawingCarousel = Docs.Create.CarouselDocument(this._drawings, { childLayoutFitWidth: true, _layout_fitWidth: true, _freeform_fitContentsToBox: true });
-        this._freeFormCanvas = Docs.Create.FreeformDocument(this._drawing, { _freeform_fitContentsToBox: true });
     };
 
     saveDrawing = async () => {
-        // const cv = DocumentView.getDocumentView(this._freeFormCanvas)?.ComponentView as CollectionFreeFormView;
-        // if (cv) {
-        // const collection = cv._marqueeViewRef.current?.collection(undefined, true, this._drawing);
-        const cIndex: number = this._drawingCarousel.carousel_index as number;
-        const focusedDrawing = this._drawings[cIndex];
-
-        const docData = focusedDrawing[DocData];
-        docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
-        docData.drawingInput = this._opts.text;
-        docData.drawingComplexity = this._opts.complexity;
-        docData.drawingColored = this._opts.autoColor;
-        docData.drawingSize = this._opts.size;
-        docData.drawingData = this._gptRes[cIndex];
-        docData.width = this._opts.size;
-        // const image = await this.getIcon(collection);
-        await this.addToPalette(focusedDrawing);
-
-        // if (collection) {
-        //     const docData = collection[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;
-        //     docData.width = this._opts.size;
-        //     // const image = await this.getIcon(collection);
-        //     await this.addToPalette(collection);
-        // }
-        // }
+        // const cIndex: number = this._drawingCarousel.carousel_index as number;
+        // const focusedDrawing = this._drawings[cIndex];
+        // const docData = focusedDrawing[DocData];
+        // docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+        // docData.drawingInput = this._opts.text;
+        // docData.drawingComplexity = this._opts.complexity;
+        // docData.drawingColored = this._opts.autoColor;
+        // docData.drawingSize = this._opts.size;
+        // docData.drawingData = this._gptRes[cIndex];
+        // docData.width = this._opts.size;
+        // await AnnotationPalette.addToPalette(focusedDrawing);
     };
 
     async getIcon(group: Doc) {
@@ -255,36 +204,13 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
         return undefined;
     }
 
-    @computed get drawingCreator() {
-        return (
-            <DocumentView
-                Document={this._freeFormCanvas}
-                addDocument={undefined}
-                addDocTab={DocumentViewInternal.addDocTabFunc}
-                pinToPres={DocumentView.PinDoc}
-                containerViewPath={returnEmptyDoclist}
-                styleProvider={DefaultStyleProvider}
-                removeDocument={returnFalse}
-                ScreenToLocalTransform={Transform.Identity}
-                PanelWidth={this.return170}
-                PanelHeight={this.return170}
-                renderDepth={1}
-                isContentActive={returnTrue}
-                focus={emptyFunction}
-                whenChildContentsActiveChanged={emptyFunction}
-                childFilters={returnEmptyFilter}
-                childFiltersByRanges={returnEmptyFilter}
-                searchFilterDocs={returnEmptyDoclist}
-            />
-        );
-    }
-
     render() {
-        return !this._display ? null : (
+        return (
             <div className="annotation-palette" style={{ zIndex: 1000 }}>
                 {this._paletteMode === 'view' && (
                     <>
                         <DocumentView
+                            ref={r => (this._docView = r)}
                             Document={Doc.MyAnnos}
                             addDocument={undefined}
                             addDocTab={DocumentViewInternal.addDocTabFunc}
@@ -303,34 +229,19 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                             childFiltersByRanges={returnEmptyFilter}
                             searchFilterDocs={returnEmptyDoclist}
                         />
-                        <Button
-                            // style={{ alignSelf: 'center' }}
-                            text="Add"
-                            icon={<FontAwesomeIcon icon="square-plus" />}
-                            // iconPlacement=""
-                            color={SettingsManager.userColor}
-                            onClick={() => this.setPaletteMode('create')}
-                        />
+                        <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} />
                     </>
                 )}
                 {this._paletteMode === 'create' && (
                     <>
                         <div style={{ display: 'flex', flexDirection: 'row', width: '170px' }}>
-                            {/* <IconButton
-                                tooltip="Advanced Options"
-                                icon={<FontAwesomeIcon icon="caret-right" />}
-                                color={SettingsManager.userColor}
-                                style={{ width: '14px' }}
-                                // onClick={() => {
-                                //     this._showOptions = !this._showOptions;
-                                // }}
-                            /> */}
                             <input
                                 aria-label="label-input"
                                 id="new-label"
                                 type="text"
                                 style={{ color: 'black', width: '170px' }}
                                 value={this._userInput}
+                                onClick={e => e.stopPropagation()}
                                 onChange={e => {
                                     this.setUserInput(e.target.value);
                                 }}
@@ -423,29 +334,9 @@ export class AnnotationPalette extends ObservableReactComponent<{}> {
                                 />
                             </div>
                         </div>
-                        <div style={{ display: 'none' }}>
-                            <DocumentView
-                                Document={this._freeFormCanvas}
-                                addDocument={undefined}
-                                addDocTab={DocumentViewInternal.addDocTabFunc}
-                                pinToPres={DocumentView.PinDoc}
-                                containerViewPath={returnEmptyDoclist}
-                                styleProvider={DefaultStyleProvider}
-                                removeDocument={returnFalse}
-                                ScreenToLocalTransform={Transform.Identity}
-                                PanelWidth={this.return170}
-                                PanelHeight={this.return170}
-                                renderDepth={1}
-                                isContentActive={returnTrue}
-                                focus={emptyFunction}
-                                whenChildContentsActiveChanged={emptyFunction}
-                                childFilters={returnEmptyFilter}
-                                childFiltersByRanges={returnEmptyFilter}
-                                searchFilterDocs={returnEmptyDoclist}
-                            />
-                        </div>
                         <DocumentView
-                            Document={this._drawingCarousel}
+                            ref={r => (this._docCarouselView = r)}
+                            Document={this._props.Document}
                             addDocument={undefined}
                             addDocTab={DocumentViewInternal.addDocTabFunc}
                             pinToPres={DocumentView.PinDoc}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 489f6f92b..ca65ea388 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -99,7 +99,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
 
     @action
     displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
-        this._selectedDoc = DocumentView.SelectedDocs().lastElement();
+        this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
         const docData = this._selectedDoc[DocData];
         this._addFunc = addFunc;
         this._deleteFunc = deleteFunc;
@@ -123,7 +123,6 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         this._size = 350;
         this._autoColor = true;
         Doc.ActiveTool = InkTool.None;
-        this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     };
 
     @action
@@ -154,8 +153,6 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         } else {
             this._showOptions = false;
             await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
-            this.hideSmartDrawHandler();
-            this._showRegenerate = true;
         }
         this._isLoading = false;
         this._canInteract = true;
@@ -176,6 +173,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             console.log(res);
             const strokeData = await this.parseResponse(res, startPt, false, autoColor);
             this._errorOccurredOnce = false;
+            this.hideSmartDrawHandler();
+            this._showRegenerate = true;
             return strokeData;
         } catch (err) {
             if (this._errorOccurredOnce) {
@@ -239,7 +238,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             });
             if (regenerate) {
                 if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
-                this._addFunc(strokeData, this._lastInput, svg[0], this._selectedDoc);
+                this._addFunc(strokeData, this._lastInput, svg[0]);
             } else {
                 this._addFunc(strokeData, this._lastInput, svg[0]);
             }
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c86342870..c3e96ab49 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -277,6 +277,7 @@ export class Doc extends RefField {
     public static get MyDashboards()          { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore
     public static get MyTemplates()           { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore
     public static get MyAnnos()               { return DocCast(Doc.UserDoc().myAnnos); } // prettier-ignore
+    public static get MyLightboxDrawings()    { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore
     public static get MyImports()             { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore
     public static get MyFilesystem()          { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore
     public static get MyTools()               { return DocCast(Doc.UserDoc().myTools); } // prettier-ignore
-- 
cgit v1.2.3-70-g09d2


From 8ca26551622d36b7856f5c1865498fa9e5d888b5 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Thu, 25 Jul 2024 23:36:52 -0400
Subject: cleaned up smart draw & palette

---
 src/.DS_Store                                      | Bin 14340 -> 14340 bytes
 src/client/documents/Documents.ts                  |   4 ++
 src/client/util/CurrentUserUtils.ts                |   3 +-
 src/client/util/bezierFit.ts                       |   2 +-
 src/client/views/DocumentButtonBar.tsx             |  13 ++--
 src/client/views/ViewBoxInterface.ts               |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   8 +--
 src/client/views/smartdraw/AnnotationPalette.tsx   |  80 +++++++++++----------
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  47 ++++++------
 9 files changed, 83 insertions(+), 76 deletions(-)

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

diff --git a/src/.DS_Store b/src/.DS_Store
index 19eea5fce..9130ba97c 100644
Binary files a/src/.DS_Store and b/src/.DS_Store differ
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ff95e38bd..d311ad971 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1009,6 +1009,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', type: 'dataviz', ...options }, undefined, undefined, undefined, overwriteDoc);
         }
 
+        export function AnnoPaletteDocument(options?: DocumentOptions) {
+            return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyAnnos]), { ...(options || {}) });
+        }
+
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
             const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeView_FreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
             documents.map(c => Doc.SetContainer(c, ret));
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 6b59828eb..8c7d40158 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -176,7 +176,7 @@ export class CurrentUserUtils {
 
     static setupLightboxDrawingPreviews(doc: Doc, field="myLightboxDrawings") {
         const reqdOpts:DocumentOptions = {
-            title: "Preview", _layout_fitWidth: true,  childLayoutFitWidth: true,
+            title: "Preview", _header_height: 0, _layout_fitWidth: true,  childLayoutFitWidth: true,
         };
         const reqdScripts = {};
         const drawings = DocCast(doc[field]);
@@ -392,6 +392,7 @@ pie title Minerals in my tap water
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts),   opts: { _width: 300, _height: 300 }},
             {key: "Chat",       creator: Docs.Create.ChatDocument,          opts: { _width: 300, _height: 300, }},
+            {key: "AnnoPalette",creator: Docs.Create.AnnoPaletteDocument,     opts: {_width: 300, _height: 300, _dropAction: dropActionType.move }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}},
             {key: "ViewSlide",  creator: slideView,                                         opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
             {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 6a4577506..693676bc3 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -694,7 +694,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             const matches: RegExpMatchArray[] = Array.from(
                 attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
             );
-            let lastPt: Point = { X: 0, Y: 0 };
+            let lastPt: Point;
             matches.forEach(match => {
                 if (match[0].startsWith('Q')) {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 6950bec14..15e90ac2a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -243,23 +243,18 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
-    @observable _annoSaved: boolean = false;
-
     @undoBatch
-    saveAnno = action((targetDoc: Doc) => {
-        // targetDoc.savedAsAnno = true;
-        this._annoSaved = true;
-        // AnnotationPalette.addToPalette(targetDoc);
+    saveAnno = action(async (targetDoc: Doc) => {
+        await AnnotationPalette.addToPalette(targetDoc);
     });
 
     @computed
     get saveAnnoButton() {
         const targetDoc = this.view0?.Document;
-        if (targetDoc && targetDoc.savedAsAnno) this._annoSaved = true;
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{this._annoSaved ? 'Saved as Annotation!' : 'Save to Annotation Palette'}</div>}>
+            <Tooltip title={<div className="dash-tooltip">{targetDoc.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette'}</div>}>
                 <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => this.saveAnno(targetDoc)}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon={this._annoSaved ? 'clipboard-check' : 'file-arrow-down'} />
+                    <FontAwesomeIcon className="documentdecorations-icon" icon={targetDoc.savedAsAnno ? 'clipboard-check' : 'file-arrow-down'} />
                 </div>
             </Tooltip>
         );
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index c633f34fb..ea5f3dd27 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -19,7 +19,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
     abstract get dataDoc(): Doc;
     abstract get fieldKey(): string;
     promoteCollection?: () => void; // moves contents of collection to parent
-    updateIcon?: () => void; // updates the icon representation of the document
+    updateIcon?: (usePanelDimensions?: boolean) => void; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
     restoreView?: (viewSpec: Doc) => boolean;
     scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f2b94fabc..a298e6ac4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1301,7 +1301,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     _height: B.height + inkWidth,
                     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
                 inkWidth,
-                stroke[1],
+                opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
                 stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
                 ActiveArrowStart(),
@@ -1911,12 +1911,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
-    updateIcon = () =>
+    updateIcon = (usePanelDimensions?: boolean) =>
         UpdateIcon(
             this.layoutDoc[Id] + '-icon' + new Date().getTime(),
             this.DocumentView?.().ContentDiv!,
-            NumCast(this.layoutDoc._width),
-            NumCast(this.layoutDoc._height),
+            usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
+            usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
             this._props.PanelWidth(),
             this._props.PanelHeight(),
             0,
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 83f173fcb..ec4279e3e 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -11,7 +11,7 @@ import { returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOn
 import { ActiveInkWidth, Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkData, InkField } from '../../../fields/InkField';
-import { BoolCast, DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { BoolCast, DocCast, ImageCast } from '../../../fields/Types';
 import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { makeUserTemplateButton, makeUserTemplateImage } from '../../util/DropConverter';
@@ -19,7 +19,7 @@ import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoable, undoBatch } from '../../util/UndoManager';
 import { CollectionFreeFormView, MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
@@ -30,9 +30,12 @@ import { ImageField } from '../../../fields/URLField';
 import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView';
 import { Copy } from '../../../fields/FieldSymbols';
 
+interface AnnotationPaletteProps {
+    Document: Doc;
+}
+
 @observer
-export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc }> {
-    static Instance: AnnotationPalette;
+export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> {
     @observable private _paletteMode: 'create' | 'view' = 'view';
     @observable private _userInput: string = '';
     @observable private _isLoading: boolean = false;
@@ -46,7 +49,6 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
     constructor(props: any) {
         super(props);
         makeObservable(this);
-        AnnotationPalette.Instance = this;
     }
 
     public static LayoutString(fieldKey: string) {
@@ -102,30 +104,35 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
         this._canInteract = true;
         this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
         this._gptRes = [];
+        this._props.Document[DocData].data = undefined;
     };
 
     public static addToPalette = async (doc: Doc) => {
-        // if (!doc.savedAsAnno) {
-        //     const clone = await Doc.MakeClone(doc);
-        //     clone.clone.title = doc.title;
-        //     const image = (await this.getIcon(doc))?.[Copy]();
-        //     if (image) {
-        //         const imageTemplate = makeUserTemplateImage(clone.clone, image);
-        //         Doc.AddDocToList(Doc.MyAnnos, 'data', imageTemplate);
-        //         doc.savedAsAnno = true;
-        //     }
-        //     this.resetPalette(true);
-        // }
+        if (!doc.savedAsAnno) {
+            const clone = await Doc.MakeClone(doc);
+            clone.clone.title = doc.title;
+            const image = (await AnnotationPalette.getIcon(doc))?.[Copy]();
+            if (image) {
+                const imageTemplate = makeUserTemplateImage(clone.clone, image);
+                Doc.AddDocToList(Doc.MyAnnos, 'data', imageTemplate);
+                doc.savedAsAnno = true;
+            }
+        }
     };
 
-    // @action
-    // displayPalette = (display: boolean) => {
-    //     this._display = display;
-    // };
+    public static getIcon(group: Doc) {
+        const docView = DocumentView.getDocumentView(group);
+        if (docView) {
+            docView.ComponentView?.updateIcon?.(true);
+            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+        }
+        return undefined;
+    }
 
     @undoBatch
     generateDrawing = action(async () => {
         this._isLoading = true;
+        this._props.Document[DocData].data = undefined;
         for (var i = 0; i < 3; i++) {
             try {
                 SmartDrawHandler.Instance._addFunc = this.createDrawing;
@@ -154,7 +161,7 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
 
         strokeList.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
-            const inkWidth = ActiveInkWidth();
+            const inkWidth = Math.min(5, ActiveInkWidth());
             const inkDoc = Docs.Create.InkDocument(
                 stroke[0],
                 { title: 'stroke',
@@ -164,7 +171,7 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
                     _height: bounds.height + inkWidth,
                     stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
                 inkWidth,
-                stroke[1],
+                opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
                 stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
                 ActiveArrowStart(),
@@ -177,28 +184,30 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
 
         const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
         if (collection) {
+            collection[DocData].freeform_fitContentsToBox = true;
             Doc.AddDocToList(this._props.Document, 'data', collection);
         }
     };
 
     saveDrawing = async () => {
-        // const cIndex: number = this._drawingCarousel.carousel_index as number;
-        // const focusedDrawing = this._drawings[cIndex];
-        // const docData = focusedDrawing[DocData];
-        // docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
-        // docData.drawingInput = this._opts.text;
-        // docData.drawingComplexity = this._opts.complexity;
-        // docData.drawingColored = this._opts.autoColor;
-        // docData.drawingSize = this._opts.size;
-        // docData.drawingData = this._gptRes[cIndex];
-        // docData.width = this._opts.size;
-        // await AnnotationPalette.addToPalette(focusedDrawing);
+        const cIndex: number = this._props.Document.carousel_index as number;
+        const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
+        const docData = focusedDrawing[DocData];
+        docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text;
+        docData.drawingInput = this._opts.text;
+        docData.drawingComplexity = this._opts.complexity;
+        docData.drawingColored = this._opts.autoColor;
+        docData.drawingSize = this._opts.size;
+        docData.drawingData = this._gptRes[cIndex];
+        docData.width = this._opts.size;
+        await AnnotationPalette.addToPalette(focusedDrawing);
+        this.resetPalette(true);
     };
 
     async getIcon(group: Doc) {
         const docView = DocumentView.getDocumentView(group);
         if (docView) {
-            docView.ComponentView?.updateIcon?.();
+            docView.ComponentView?.updateIcon?.(true);
             return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
         }
         return undefined;
@@ -206,7 +215,7 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
 
     render() {
         return (
-            <div className="annotation-palette" style={{ zIndex: 1000 }}>
+            <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
                 {this._paletteMode === 'view' && (
                     <>
                         <DocumentView
@@ -241,7 +250,6 @@ export class AnnotationPalette extends ObservableReactComponent<{ Document: Doc
                                 type="text"
                                 style={{ color: 'black', width: '170px' }}
                                 value={this._userInput}
-                                onClick={e => e.stopPropagation()}
                                 onChange={e => {
                                     this.setUserInput(e.target.value);
                                 }}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index ca65ea388..c842551c3 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -51,6 +51,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
     private _selectedDoc: Doc | undefined = undefined;
+    private _errorOccurredOnce = false;
 
     constructor(props: any) {
         super(props);
@@ -152,39 +153,37 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             this._showEditBox = false;
         } else {
             this._showOptions = false;
-            await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+            try {
+                await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+                this._showRegenerate = true;
+                this.hideSmartDrawHandler();
+            } catch (err) {
+                if (this._errorOccurredOnce) {
+                    console.error('GPT call failed', err);
+                    this._errorOccurredOnce = false;
+                } else {
+                    this._errorOccurredOnce = true;
+                    await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+                }
+            }
         }
         this._isLoading = false;
         this._canInteract = true;
     };
 
-    _errorOccurredOnce = false;
     @action
     drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
         if (input === '') return;
         this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
-
-        try {
-            const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
-            if (!res) {
-                console.error('GPT call failed');
-                return;
-            }
-            console.log(res);
-            const strokeData = await this.parseResponse(res, startPt, false, autoColor);
-            this._errorOccurredOnce = false;
-            this.hideSmartDrawHandler();
-            this._showRegenerate = true;
-            return strokeData;
-        } catch (err) {
-            if (this._errorOccurredOnce) {
-                console.error('GPT call failed', err);
-                this._errorOccurredOnce = false;
-            } else {
-                this._errorOccurredOnce = true;
-                this.drawWithGPT(startPt, input, complexity, size, autoColor);
-            }
+        const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
+        if (!res) {
+            console.error('GPT call failed');
+            return;
         }
+        console.log(res);
+        const strokeData = await this.parseResponse(res, startPt, false, autoColor);
+        this._errorOccurredOnce = false;
+        return strokeData;
     };
 
     @action
@@ -212,7 +211,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log(res);
-            this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+            await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
         } catch (err) {
             console.error('GPT call failed', err);
         }
-- 
cgit v1.2.3-70-g09d2


From fd5278045e8c2e280d81cb965c0b2cc5afb59be8 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 7 Aug 2024 18:09:47 -0400
Subject: problem with setting smooth amount

---
 package-lock.json                                  |   6 +
 package.json                                       |   1 +
 src/client/apis/gpt/GPT.ts                         |  45 +++++-
 src/client/cognitive_services/CognitiveServices.ts |  18 ++-
 src/client/views/GestureOverlay.tsx                | 150 ++++++++---------
 src/client/views/InkStrokeProperties.ts            |  43 ++++-
 src/client/views/InkTranscription.tsx              |   4 +-
 src/client/views/InkingStroke.tsx                  |   4 +-
 src/client/views/PropertiesButtons.tsx             |   4 +-
 src/client/views/PropertiesView.scss               |   6 +
 src/client/views/PropertiesView.tsx                | 180 +++++++++++++++++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 106 ++++++------
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |   2 +
 .../collections/collectionFreeForm/MarqueeView.tsx |  80 ++++++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   8 +
 src/client/views/pdf/AnchorMenu.tsx                |  38 +++++
 src/client/views/pdf/PDFViewer.tsx                 |  10 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   |  44 +----
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 155 ++++++++++++++----
 19 files changed, 669 insertions(+), 235 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index 70f53156c..7f6237ef5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -214,6 +214,7 @@
                 "sass-loader": "^14.2.0",
                 "serializr": "^3.0.2",
                 "shelljs": "^0.8.5",
+                "simplify-js": "^1.2.4",
                 "socket.io": "^4.7.2",
                 "socket.io-client": "^4.7.2",
                 "standard-http-error": "^2.0.1",
@@ -38407,6 +38408,11 @@
                 "node": ">=10"
             }
         },
+        "node_modules/simplify-js": {
+            "version": "1.2.4",
+            "resolved": "https://registry.npmjs.org/simplify-js/-/simplify-js-1.2.4.tgz",
+            "integrity": "sha512-vITfSlwt7h/oyrU42R83mtzFpwYk3+mkH9bOHqq/Qw6n8rtR7aE3NZQ5fbcyCUVVmuMJR6ynsAhOfK2qoah8Jg=="
+        },
         "node_modules/skmeans": {
             "version": "0.9.7",
             "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz",
diff --git a/package.json b/package.json
index 8929bddf0..16b2841be 100644
--- a/package.json
+++ b/package.json
@@ -299,6 +299,7 @@
         "sass-loader": "^14.2.0",
         "serializr": "^3.0.2",
         "shelljs": "^0.8.5",
+        "simplify-js": "^1.2.4",
         "socket.io": "^4.7.2",
         "socket.io-client": "^4.7.2",
         "standard-http-error": "^2.0.1",
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index a780596fa..ee8d5e9b2 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -13,6 +13,7 @@ enum GPTCallType {
     MERMAID = 'mermaid',
     DATA = 'data',
     DRAW = 'draw',
+    COLOR = 'color',
 }
 
 type GPTCallOpts = {
@@ -57,9 +58,15 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
     draw: {
         model: 'gpt-4o',
         maxTokens: 1024,
-        temp: 0.5,
+        temp: 0.8,
         prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, polygon, and path with M, Q, C, and L so only use those.',
     },
+    color: {
+        model: 'gpt-4o',
+        maxTokens: 1024,
+        temp: 0.5,
+        prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
+    },
 };
 
 let lastCall = '';
@@ -187,4 +194,38 @@ const gptHandwriting = async (src: string): Promise<string> => {
     }
 };
 
-export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting };
+const gptDrawingColor = async (image: string, coords: 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 the drawing in the image represents in 1-5 words. Then, given a list of a list of coordinates, where each list is the coordinates for one stroke of the drawing, determine which part of the drawing it is. Return just what the item it is, followed by ~~~ then only your descriptions in a list like [description, description, ...]. Here are the coordinates: ${coords}`,
+                        },
+                        {
+                            type: 'image_url',
+                            image_url: {
+                                url: `${image}`,
+                                detail: 'low',
+                            },
+                        },
+                    ],
+                },
+            ],
+        });
+        if (response.choices[0].message.content) {
+            return response.choices[0].message.content;
+        }
+        return 'Missing labels';
+    } catch (err) {
+        console.log(err);
+        return 'Error connecting with API';
+    }
+};
+
+export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting, gptDrawingColor };
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9f46b8685..9f7701c54 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -47,14 +47,14 @@ export namespace CognitiveServices {
     const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
         const apiKey = process.env[service.toUpperCase()];
         if (apiKey) {
-            console.log(data)
+            console.log(data);
             console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`);
             return undefined;
         }
 
         let results: any;
         try {
-            results = await manager.requester("has", manager.converter(data), service).then(json => JSON.parse(json));
+            results = await manager.requester('has', manager.converter(data), service).then(json => JSON.parse(json));
         } catch (e) {
             throw e;
         }
@@ -138,12 +138,14 @@ export namespace CognitiveServices {
                     points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(','),
                     language: 'en-US',
                 }));
-                console.log(JSON.stringify({
-                    version: 1,
-                    language: 'en-US',
-                    unit: 'mm',
-                    strokes,
-                }))
+                console.log(
+                    JSON.stringify({
+                        version: 1,
+                        language: 'en-US',
+                        unit: 'mm',
+                        strokes,
+                    })
+                );
                 return JSON.stringify({
                     version: 1,
                     language: 'en-US',
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 649208989..7eac583dd 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -141,7 +141,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
 
             // if any of the shape is activated in the CollectionFreeFormViewChrome
             if (this.InkShape) {
-                this.makeBezierPolygon(this.InkShape, false);
+                GestureOverlay.makeBezierPolygon(this._points, this.InkShape, false);
                 this.dispatchGesture(this.InkShape);
                 this.primCreated();
             }
@@ -158,7 +158,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                         case Gestures.Triangle:
                         case Gestures.Rectangle:
                         case Gestures.Circle:
-                            this.makeBezierPolygon(result.Name, true);
+                            GestureOverlay.makeBezierPolygon(this._points, result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
                             console.log(result.Name);
                             console.log();
@@ -202,17 +202,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         this._points.length = 0;
     };
 
-    makeBezierPolygon = (shape: string, gesture: boolean) => {
-        const xs = this._points.map(p => p.X);
-        const ys = this._points.map(p => p.Y);
+    public static makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => {
+        const xs = points.map(p => p.X);
+        const ys = points.map(p => p.Y);
         let right = Math.max(...xs);
         let left = Math.min(...xs);
         let bottom = Math.max(...ys);
         let top = Math.min(...ys);
-        const firstx = this._points[0].X;
-        const firsty = this._points[0].Y;
-        let lastx = this._points[this._points.length - 2].X;
-        let lasty = this._points[this._points.length - 2].Y;
+        const firstx = points[0].X;
+        const firsty = points[0].Y;
+        let lastx = points[points.length - 2].X;
+        let lasty = points[points.length - 2].Y;
         let fourth = (lastx - firstx) / 4;
         if (isNaN(fourth) || fourth === 0) {
             fourth = 0.01;
@@ -223,15 +223,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
         }
         // const b = firsty - m * firstx;
         if (shape === 'noRec') {
-            return false;
+            return undefined;
         }
         if (!gesture) {
             // if shape options is activated in inkOptionMenu
             // take second to last point because _point[length-1] is _points[0]
-            right = this._points[this._points.length - 2].X;
-            left = this._points[0].X;
-            bottom = this._points[this._points.length - 2].Y;
-            top = this._points[0].Y;
+            right = points[points.length - 2].X;
+            left = points[0].X;
+            bottom = points[points.length - 2].Y;
+            top = points[0].Y;
             if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
                 if (left > right) {
                     const temp = right;
@@ -245,47 +245,47 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 }
             }
         }
-        this._points.length = 0;
+        points.length = 0;
         switch (shape) {
             case Gestures.Rectangle:
-                this._points.push({ X: left, Y: top }); // curr pt
-                this._points.push({ X: left, Y: top }); // curr first ctrl pt
-                this._points.push({ X: right, Y: top }); // next ctrl pt
-                this._points.push({ X: right, Y: top }); // next pt
-
-                this._points.push({ X: right, Y: top }); // next pt
-                this._points.push({ X: right, Y: top }); // next first ctrl pt
-                this._points.push({ X: right, Y: bottom }); // next next ctrl pt
-                this._points.push({ X: right, Y: bottom }); // next next pt
-
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
-
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: top });
-                this._points.push({ X: left, Y: top });
+                points.push({ X: left, Y: top }); // curr pt
+                points.push({ X: left, Y: top }); // curr first ctrl pt
+                points.push({ X: right, Y: top }); // next ctrl pt
+                points.push({ X: right, Y: top }); // next pt
+
+                points.push({ X: right, Y: top }); // next pt
+                points.push({ X: right, Y: top }); // next first ctrl pt
+                points.push({ X: right, Y: bottom }); // next next ctrl pt
+                points.push({ X: right, Y: bottom }); // next next pt
+
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: top });
+                points.push({ X: left, Y: top });
 
                 break;
 
             case Gestures.Triangle:
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
 
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
-                this._points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
+                points.push({ X: right, Y: bottom });
 
-                this._points.push({ X: (right + left) / 2, Y: top });
-                this._points.push({ X: (right + left) / 2, Y: top });
-                this._points.push({ X: (right + left) / 2, Y: top });
-                this._points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
+                points.push({ X: (right + left) / 2, Y: top });
 
-                this._points.push({ X: left, Y: bottom });
-                this._points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
+                points.push({ X: left, Y: bottom });
 
                 break;
             case Gestures.Circle:
@@ -299,25 +299,25 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
 
                     // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
-                    this._points.push({ X: centerX, Y: centerY + radius }); // curr pt
-                    this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
-                    this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
-                    this._points.push({ X: centerX + radius, Y: centerY }); // next pt
-
-                    this._points.push({ X: centerX + radius, Y: centerY }); // next pt
-                    this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
-                    this._points.push({ X: centerX + c * radius, Y: centerY - radius });
-                    this._points.push({ X: centerX, Y: centerY - radius });
-
-                    this._points.push({ X: centerX, Y: centerY - radius });
-                    this._points.push({ X: centerX - c * radius, Y: centerY - radius });
-                    this._points.push({ X: centerX - radius, Y: centerY - c * radius });
-                    this._points.push({ X: centerX - radius, Y: centerY });
-
-                    this._points.push({ X: centerX - radius, Y: centerY });
-                    this._points.push({ X: centerX - radius, Y: centerY + c * radius });
-                    this._points.push({ X: centerX - c * radius, Y: centerY + radius });
-                    this._points.push({ X: centerX, Y: centerY + radius });
+                    points.push({ X: centerX, Y: centerY + radius }); // curr pt
+                    points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+                    points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+                    points.push({ X: centerX + radius, Y: centerY }); // next pt
+
+                    points.push({ X: centerX + radius, Y: centerY }); // next pt
+                    points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
+                    points.push({ X: centerX + c * radius, Y: centerY - radius });
+                    points.push({ X: centerX, Y: centerY - radius });
+
+                    points.push({ X: centerX, Y: centerY - radius });
+                    points.push({ X: centerX - c * radius, Y: centerY - radius });
+                    points.push({ X: centerX - radius, Y: centerY - c * radius });
+                    points.push({ X: centerX - radius, Y: centerY });
+
+                    points.push({ X: centerX - radius, Y: centerY });
+                    points.push({ X: centerX - radius, Y: centerY + c * radius });
+                    points.push({ X: centerX - c * radius, Y: centerY + radius });
+                    points.push({ X: centerX, Y: centerY + radius });
                 }
                 break;
 
@@ -328,11 +328,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                 if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) {
                     lasty = firsty;
                 }
-                this._points.push({ X: firstx, Y: firsty });
-                this._points.push({ X: firstx, Y: firsty });
+                points.push({ X: firstx, Y: firsty });
+                points.push({ X: firstx, Y: firsty });
 
-                this._points.push({ X: lastx, Y: lasty });
-                this._points.push({ X: lastx, Y: lasty });
+                points.push({ X: lastx, Y: lasty });
+                points.push({ X: lastx, Y: lasty });
                 break;
             case Gestures.Arrow:
                 {
@@ -347,16 +347,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                     const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
                     const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
                     const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
-                    this._points.push({ X: x1, Y: y1 });
-                    this._points.push({ X: x2, Y: y2 });
-                    this._points.push({ X: x3, Y: y3 });
-                    this._points.push({ X: x4, Y: y4 });
-                    this._points.push({ X: x2, Y: y2 });
+                    points.push({ X: x1, Y: y1 });
+                    points.push({ X: x2, Y: y2 });
+                    points.push({ X: x3, Y: y3 });
+                    points.push({ X: x4, Y: y4 });
+                    points.push({ X: x2, Y: y2 });
                 }
                 break;
             default:
         }
-        return false;
+        return points;
     };
 
     dispatchGesture = (gesture: Gestures, stroke?: InkData, text?: any) => {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 3920ecc2a..35d628a4e 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -5,8 +5,8 @@ import { Doc, NumListCast, Opt } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
-import { Cast, NumCast } from '../../fields/Types';
-import { PointData } from '../../pen-gestures/GestureTypes';
+import { Cast, NumCast, toList } from '../../fields/Types';
+import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
 import { undoBatch } from '../util/UndoManager';
@@ -14,6 +14,9 @@ import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
+import simplify from 'simplify-js';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+import { GestureOverlay } from './GestureOverlay';
 
 export class InkStrokeProperties {
     // eslint-disable-next-line no-use-before-define
@@ -487,4 +490,40 @@ export class InkStrokeProperties {
             }
             return inkCopy;
         });
+
+    @undoBatch
+    smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => {
+        inkDocs.forEach(inkDoc => {
+            const inkView = DocumentView.getDocumentView(inkDoc);
+            const inkStroke = inkView?.ComponentView as InkingStroke;
+            const { inkData } = inkStroke.inkScaledData();
+
+            const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
+            console.log(result);
+            let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
+            if (result && (result.Name === 'line' ? result.Score > 0.92 : result.Score > 0.85)) {
+                switch (result.Name) {
+                    case Gestures.Line:
+                    case Gestures.Triangle:
+                    case Gestures.Rectangle:
+                    case Gestures.Circle:
+                        GestureOverlay.makeBezierPolygon(inkData, result.Name, true);
+                        break;
+                    default:
+                }
+            } else {
+                const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore
+                if (polylinePoints.length > 2) {
+                    const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore
+                    for (var i = 4; i < inkData.length - 3; i += 4) {
+                        const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y);
+                        if (!contains) {
+                            this._currentPoint = i;
+                            inkView && this.deletePoints(inkView, false);
+                        }
+                    }
+                }
+            }
+        });
+    };
 }
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 3f90df7d1..1277a6fe7 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -6,7 +6,7 @@ import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { Cast, DateCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
 import { aggregateBounds } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
-import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm';
 import { InkingStroke } from './InkingStroke';
 import './InkTranscription.scss';
 import { Docs } from '../documents/Documents';
@@ -387,7 +387,7 @@ export class InkTranscription extends React.Component {
         );
         docView.props.removeDocument?.(selected);
         // Gets a collection based on the selected nodes using a marquee view ref
-        const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+        const newCollection = MarqueeView.getCollection(selected, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
         if (newCollection) {
             newCollection.width = NumCast(newCollection._width);
             newCollection.height = NumCast(newCollection._height);
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ce1c07f2f..48b26d903 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -104,10 +104,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
      * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
      * and the recognized words to the 'handwriting'
      */
-    analyzeStrokes=()=> {
+    analyzeStrokes = () => {
         const data: InkData = this.inkScaledData().inkData ?? [];
         CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
-    }
+    };
 
     /**
      * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index edf6df2b9..de1800700 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -15,7 +15,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit
 import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast } from '../../fields/Types';
@@ -30,6 +30,8 @@ import { Colors } from './global/globalEnums';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
+import { InkStrokeProperties } from './InkStrokeProperties';
 
 @observer
 export class PropertiesButtons extends React.Component<{}, {}> {
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 840df41e7..aa825a6e9 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -638,3 +638,9 @@
     padding-left: 8px;
     background-color: rgb(51, 51, 51);
 }
+
+.smooth,
+.color,
+.smooth-slider {
+    margin-top: 3px;
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 024db82a4..ac2625f32 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -5,7 +5,7 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core';
 import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox, Tooltip } from '@mui/material';
-import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
+import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components';
 import { concat } from 'lodash';
 import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -14,13 +14,13 @@ import { ColorResult, SketchPicker } from 'react-color';
 import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
 import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
 import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ComputedField } from '../../fields/ScriptField';
-import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { GroupManager } from '../util/GroupManager';
@@ -43,6 +43,10 @@ import { DocumentView } from './nodes/DocumentView';
 import { StyleProviderFuncType } from './nodes/FieldView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+import { InkingStroke } from './InkingStroke';
+import { SettingsManager } from '../util/SettingsManager';
+import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
 
 const _global = (window /* browser */ || global) /* node */ as any;
 
@@ -117,6 +121,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @observable openAddSlide: boolean = false;
     @observable openSlideOptions: boolean = false;
 
+    // For ink groups
+    @observable containsInkDoc: boolean = false;
+    @observable inkDoc: Doc | undefined = undefined;
+
     @observable _controlButton: boolean = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
 
@@ -794,7 +802,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     getField(key: string) {
-        return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
+        return this.containsInkDoc ? Field.toString(this.inkDoc?.[DocData][key] as FieldType) : Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
     }
 
     @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
@@ -805,8 +813,17 @@ 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.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
-    set strokeThk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); } // prettier-ignore
+    @computed get strokeThk(){ return this.containsInkDoc ?  NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
+    set strokeThk(value) { 
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_width = Math.round(value * 100) / 100;
+            })
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); 
+        }
+    } // prettier-ignore
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
@@ -843,10 +860,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
 
     private _lastDash: any = '2';
 
-    @computed get colorFil() { return StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
-    set colorFil(value) { this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); } // prettier-ignore
-    @computed get colorStk() { return StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
-    set colorStk(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
+    @computed get colorFil() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].fillColor) : StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
+    set colorFil(value) {
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke;
+                const { inkData } = inkStroke.inkScaledData();
+                if (InkingStroke.IsClosed(inkData)) {
+                    doc[DocData].fillColor = value || undefined;
+                }
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined);
+        }
+    }
+    @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
+    set colorStk(value) {
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].color = value || undefined;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined);
+        }
+    }
 
     colorButton(value: string, type: string, setter: () => void) {
         return (
@@ -917,10 +956,69 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
-    @computed get dashdStk() {  return this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
+    @computed get smoothAndColor() {
+        const targetDoc = this.selectedLayoutDoc;
+        return (
+            <div>
+                <div className="smooth">
+                    <Toggle
+                        text={'Smooth Ink Strokes'}
+                        color={SettingsManager.userColor}
+                        icon={<FontAwesomeIcon icon="bezier-curve" />}
+                        iconPlacement="left"
+                        align="flex-start"
+                        fillWidth
+                        toggleType={ToggleType.BUTTON}
+                        onClick={undoable(() => {
+                            InkStrokeProperties.Instance.smoothInkStrokes(this.containsInkDoc ? DocListCast(targetDoc.data) : [targetDoc], this.smoothAmt);
+                        }, 'smoothStrokes')}
+                    />
+                </div>
+                <div className="smooth-slider">
+                    {this.getNumber(
+                        'Smooth Amount',
+                        '',
+                        1,
+                        Math.max(20, this.smoothAmt),
+                        this.smoothAmt,
+                        (val: number) => {
+                            !isNaN(val) && (this.smoothAmt = val);
+                        },
+                        20,
+                        1
+                    )}
+                </div>
+                {!targetDoc.layout_isSvg && (
+                    <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);
+                            }, 'smoothStrokes')}
+                        />
+                    </div>
+                )}
+            </div>
+        );
+    }
+
+    @computed get dashdStk() {  return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
     set dashdStk(value) {
         value && (this._lastDash = value);
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_dash = value ? this._lastDash : undefined;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+        }
     }
     @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
     set widthStk(value) {
@@ -930,13 +1028,31 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     set markScal(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
     }
+    @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '1'); } // prettier-ignore
+    set smoothAmt(value) {
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value));
+    }
     @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
     set markHead(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_startMarker = value;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+        }
     }
     @computed get markTail() {  return this.getField('stroke_endMarker') || ''; } // prettier-ignore
     set markTail(value) {
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_endMarker = value;
+            });
+        } else {
+            this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+        }
     }
 
     regInput = (key: string, value: any, setter: (val: string) => {}) => (
@@ -1036,6 +1152,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             <div className="appearance-editor">
                 {this.widthAndDash}
                 {this.strokeAndFill}
+                {this.smoothAndColor}
+            </div>
+        );
+    }
+
+    @computed get inkEditor() {
+        return (
+            <div className="ink-editor">
+                {this.widthAndDash}
+                {this.strokeAndFill}
             </div>
         );
     }
@@ -1164,6 +1290,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
 
     @computed get inkSubMenu() {
+        this.containsInkDoc = false;
         return (
             // prettier-ignore
             <>
@@ -1177,6 +1304,30 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
+    containsInk = (selectedDoc: Doc) => {
+        const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data);
+        for (var i = 0; i < childDocs.length; i++) {
+            if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) {
+                this.inkDoc = childDocs[i];
+                this.containsInkDoc = true;
+                return true;
+            }
+        }
+        this.containsInkDoc = false;
+        return false;
+    };
+
+    @computed get inkCollectionSubMenu() {
+        return (
+            // prettier-ignore
+            <>
+                <PropertiesSection title="Ink Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
+                    {this.isGroup && this.containsInk(this.selectedDoc) ? this.appearanceEditor : null}
+                </PropertiesSection>
+            </>
+        );
+    }
+
     @computed get fieldsSubMenu() {
         return (
             <PropertiesSection
@@ -1737,6 +1888,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                     {this.linksSubMenu}
                     {!this.selectedLink || !this.openLinks ? null : this.linkProperties}
                     {this.inkSubMenu}
+                    {this.inkCollectionSubMenu}
                     {this.contextsSubMenu}
                     {isNovice ? null : this.sharingSubMenu}
                     {this.filtersSubMenu}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a298e6ac4..8575807b3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -548,24 +548,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             default: {
                 const { points } = ge;
                 const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
-                const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-                const inkDoc = Docs.Create.InkDocument(
-                    points,
-                    { title: ge.gesture.toString(),
-                      x: B.x - inkWidth / 2,
-                      y: B.y - inkWidth / 2,
-                      _width: B.width + inkWidth,
-                      _height: B.height + inkWidth,
-                      stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                    inkWidth,
-                    ActiveInkColor(),
-                    ActiveInkBezierApprox(),
-                    ActiveFillColor(),
-                    ActiveArrowStart(),
-                    ActiveArrowEnd(),
-                    ActiveDash(),
-                    ActiveIsInkMask()
-                );
+                const inkDoc = this.createInkDoc(points, B);
                 if (Doc.ActiveTool === InkTool.Write) {
                     this.unprocessedDocs.push(inkDoc);
                     CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
@@ -652,26 +635,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
                         const newStrokes = segments?.map(segment => {
                             const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
-                            const bounds = InkField.getBounds(points);
-                            const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
-                            const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-                            return Docs.Create.InkDocument(
-                                points,
-                                { title: 'stroke',
-                            x: B.x - inkWidth / 2,
-                            y: B.y - inkWidth / 2,
-                            _width: B.width + inkWidth,
-                            _height: B.height + inkWidth,
-                            stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                                inkWidth,
-                                ActiveInkColor(),
-                                ActiveInkBezierApprox(),
-                                ActiveFillColor(),
-                                ActiveArrowStart(),
-                                ActiveArrowEnd(),
-                                ActiveDash(),
-                                ActiveIsInkMask()
-                            );
+                            return this.createInkDoc(points);
                         });
                         newStrokes && this.addDocument?.(newStrokes);
                         // setTimeout(() => this._eraserLock--);
@@ -1276,18 +1240,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
+    createInkDoc = (points: InkData, transformedBounds?: { x: number; y: number; width: number; height: number }) => {
+        const bounds = InkField.getBounds(points);
+        const B = transformedBounds || this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+        const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+        return Docs.Create.InkDocument(
+            points,
+            { title: 'stroke',
+                x: B.x - inkWidth / 2,
+                y: B.y - inkWidth / 2,
+                _width: B.width + inkWidth,
+                _height: B.height + inkWidth,
+                stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+            inkWidth,
+            ActiveInkColor(),
+            ActiveInkBezierApprox(),
+            ActiveFillColor(),
+            ActiveArrowStart(),
+            ActiveArrowEnd(),
+            ActiveDash(),
+            ActiveIsInkMask()
+        );
+    };
+
     @action
-    showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => {
-        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing);
+    showSmartDraw = (e: PointerEvent) => {
+        SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+        SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+        SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
     };
 
     _drawing: Doc[] = [];
     _drawingContainer: Doc | undefined = undefined;
     @undoBatch
-    createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
+    createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
         this._drawing = [];
         const xf = this.screenToFreeformContentsXf;
-        // this._drawingContainer = undefined;
         strokeData.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
             const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
@@ -1310,20 +1299,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ActiveIsInkMask()
             );
             this._drawing.push(inkDoc);
-            this.addDocument(inkDoc);
         });
-        const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
-        if (collection) {
-            const docData = collection[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;
-            this._drawingContainer = collection;
-        }
-        this._batch?.end();
+        const collection = MarqueeView.getCollection(this._drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+        return collection;
     };
 
     removeDrawing = (doc?: Doc) => {
@@ -1339,6 +1317,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._drawing = [];
     };
 
+    addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
+        const docData = doc[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;
+        this._drawingContainer = doc;
+        this.addDocument(doc);
+        this._batch?.end();
+    };
+
     @action
     zoom = (pointX: number, pointY: number, deltaY: number): void => {
         if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
@@ -2045,7 +2036,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         optionItems.push({
             description: 'Show Drawing Editor',
             event: action(() => {
-                !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+                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',
         });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index b3fdd9379..76c37dff0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -21,6 +21,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
     public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
     public classifyImages: (e: React.MouseEvent | undefined) => void = unimplementedFunction;
     public groupImages: () => void = unimplementedFunction;
+    public smoothStrokes: (docs?: Doc[]) => void = unimplementedFunction;
     public isShown = () => this._opacity > 0;
     constructor(props: any) {
         super(props);
@@ -41,6 +42,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
                 <IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
                 <IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
                 <IconButton tooltip="Classify Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
+                <IconButton tooltip="Smooth Strokes" onPointerDown={() => this.smoothStrokes} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
             </>
         );
         return this.getElement(buttons);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 8560323c9..92c0da983 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -4,14 +4,14 @@ import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils';
-import { intersectRect, numberRange } from '../../../../Utils';
+import { intersectRect, numberRange, unimplementedFunction } from '../../../../Utils';
 import { Doc, NumListCast, Opt } from '../../../../fields/Doc';
 import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
 import { RichTextField } from '../../../../fields/RichTextField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT';
@@ -26,7 +26,7 @@ import { ContextMenu } from '../../ContextMenu';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
-import { DocumentView } from '../../nodes/DocumentView';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../../nodes/DocumentView';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -36,7 +36,12 @@ import { CollectionFreeFormView } from './CollectionFreeFormView';
 import { ImageLabelHandler } from './ImageLabelHandler';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './MarqueeView.scss';
-import { collectionOf } from '@turf/turf';
+import { collectionOf, points } from '@turf/turf';
+import { InkingStroke } from '../../InkingStroke';
+import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
+import { Gestures } from '../../../../pen-gestures/GestureTypes';
+import { GestureOverlay } from '../../GestureOverlay';
+import { InkStrokeProperties } from '../../InkStrokeProperties';
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -89,6 +94,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         return bounds;
     }
 
+    public AddInkDoc: (points: InkData) => Doc | void = unimplementedFunction;
+
     componentDidMount() {
         this._props.setPreviewCursor?.(this.setPreviewCursor);
     }
@@ -277,6 +284,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView;
             MarqueeOptionsMenu.Instance.classifyImages = this.classifyImages;
             MarqueeOptionsMenu.Instance.groupImages = this.groupImages;
+            MarqueeOptionsMenu.Instance.smoothStrokes = this.smoothStrokes;
             document.addEventListener('pointerdown', hideMarquee, true);
             document.addEventListener('wheel', hideMarquee, true);
         } else {
@@ -489,6 +497,70 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this._props.Document.pivotField = 'data_label';
     });
 
+    @undoBatch
+    smoothStrokes = action((docs?: Doc[]) => {
+        docs && docs.length > 0 ? (this._selectedDocs = docs) : (this._selectedDocs = this.marqueeSelect(false, DocumentType.INK));
+        if (this._selectedDocs.length == 0) return;
+
+        this._selectedDocs.forEach(stroke => {
+            const docView = DocumentView.getDocumentView(stroke);
+            const inkStroke = docView?.ComponentView as InkingStroke;
+            const { inkData } = inkStroke.inkScaledData();
+
+            const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
+            console.log(result);
+            let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
+            if (result && (result.Name === 'line' ? result.Score > 0.9 : result.Score > 0.8)) {
+                switch (result.Name) {
+                    case Gestures.Line:
+                    case Gestures.Triangle:
+                    case Gestures.Rectangle:
+                    case Gestures.Circle:
+                        GestureOverlay.makeBezierPolygon(inkData, result.Name, true);
+                        break;
+                    default:
+                }
+            } else {
+                const distances: number[] = [];
+                for (var i = 0; i < inkData.length - 3; i += 4) {
+                    distances.push(Math.sqrt((inkData[i].X - inkData[i + 3].X) ** 2 + (inkData[i].Y - inkData[i + 3].Y) ** 2));
+                }
+                const avgDist = (NumCast(stroke.width) + NumCast(stroke.height)) / 2;
+                // const avgDist = distances.reduce((a, b) => a + b) / distances.length;
+                if (Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) < avgDist) {
+                    inkData.pop();
+                    inkData.push({ X: inkData[0].X, Y: inkData[0].Y });
+                }
+                // const editedPoints: InkData = [];
+                // const toDelete: number[] = [];
+
+                // distances.forEach((dist, i) => {
+                //     if (dist < avgDist / 3) {
+                //         toDelete.unshift(i * 4);
+                //     }
+                // });
+                // toDelete.forEach(pt => {
+                //     InkStrokeProperties.Instance._currentPoint = pt;
+                //     docView && InkStrokeProperties.Instance.deletePoints(docView, false);
+                // });
+
+                // for (var i = 0; i < distances.length; i++) {
+                // if (distances[i] > avgDist / 3) {
+                //     editedPoints.push(...inkData.slice(i * 4, i * 4 + 4));
+                // } else {
+                //     if (i !== distances.length) {
+                //         editedPoints.push(...inkData.slice(i * 4, i * 4 + 2));
+                //         editedPoints.push(...inkData.slice(i * 4 + 6, i * 4 + 8));
+                //         i++;
+                //     }
+                // }
+                // }
+                // inkData.length = 0;
+                // inkData.push(...editedPoints);
+            }
+        });
+    });
+
     @undoBatch
     syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9f2a9b8e1..1e700d240 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -295,6 +295,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
         });
 
+        AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
+            const container = DocCast(this._props.Document.embedContainer);
+            const docView = DocumentView.getDocumentView?.(container);
+            docView?.ComponentView?._props.addDocument?.(drawing);
+            drawing.x = NumCast(this._props.Document.x) + (this._props.Document.width as number);
+            drawing.y = NumCast(this._props.Document.y);
+        };
+
         AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
         const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
         this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index df990b0c0..ea574493a 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -15,6 +15,11 @@ import { LinkPopup } from '../linking/LinkPopup';
 import { DocumentView } from '../nodes/DocumentView';
 import './AnchorMenu.scss';
 import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
+import { InkData, InkField } from '../../../fields/InkField';
+import { DocData } from '../../../fields/DocSymbols';
+import { undoBatch } from '../../util/UndoManager';
+import ReactLoading from 'react-loading';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -38,6 +43,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
 
     // GPT additions
     @observable private _selectedText: string = '';
+    @observable private _isLoading: boolean = false;
     @action
     public setSelectedText = (txt: string) => {
         this._selectedText = txt.trim();
@@ -61,6 +67,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public get Active() {
         return this._left > 0;
     }
+    public AddDrawingAnnotation: (doc: Doc) => void = unimplementedFunction;
     public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
 
     componentWillUnmount() {
@@ -137,6 +144,29 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.addToCollection?.(newCol);
     };
 
+    gptDraw = async (e: React.PointerEvent) => {
+        try {
+            SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
+            this._isLoading = true;
+            await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
+            this._isLoading = false;
+        } catch (err) {
+            console.error(err);
+        }
+    };
+
+    @undoBatch
+    createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+        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;
+    });
+
     pointerDown = (e: React.PointerEvent) => {
         setupMoveUpEvents(
             this,
@@ -239,6 +269,14 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                         icon={<FontAwesomeIcon icon="id-card" size="lg" />}
                         color={SettingsManager.userColor}
                     />
+                    {this._selectedText && (
+                        <IconButton
+                            tooltip="Create drawing"
+                            onPointerDown={e => this.gptDraw(e)}
+                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon="paintbrush" size="lg" />}
+                            color={SettingsManager.userColor}
+                        />
+                    )}
                     {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
                         <IconButton
                             tooltip="Click to Record Annotation" //
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index fbe3518ec..5d526c1bf 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -33,7 +33,7 @@ import './PDFViewer.scss';
 
 // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
 // The workerSrc property shall be specified.
-Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.3.136/build/pdf.worker.mjs';
+Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.mjs';
 
 interface IViewerProps extends FieldViewProps {
     pdfBox: PDFBox;
@@ -435,6 +435,14 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
         // allows for creating collection
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
+        AnchorMenu.Instance.AddDrawingAnnotation = this.addDrawingAnnotation;
+    };
+
+    addDrawingAnnotation = (drawing: Doc) => {
+        // drawing[DocData].x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX
+        // const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
+        drawing.y = (drawing.y as number) + (this._props.Document.data_sidebar_panY as number);
+        this._props.addDocument?.(drawing);
     };
 
     @action
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index ec4279e3e..7e4d46204 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -64,7 +64,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     @action
     handleKeyPress = async (event: React.KeyboardEvent) => {
         if (event.key === 'Enter') {
-            await this.generateDrawing();
+            await this.generateDrawings();
         }
     };
 
@@ -130,15 +130,14 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     }
 
     @undoBatch
-    generateDrawing = action(async () => {
+    generateDrawings = action(async () => {
         this._isLoading = true;
         this._props.Document[DocData].data = undefined;
         for (var i = 0; i < 3; i++) {
             try {
-                SmartDrawHandler.Instance._addFunc = this.createDrawing;
+                SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
                 this._canInteract = false;
                 if (this._showRegenerate) {
-                    SmartDrawHandler.Instance._deleteFunc = unimplementedFunction;
                     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);
@@ -154,39 +153,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     });
 
     @action
-    createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
-        this._opts = opts;
+    addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
         this._gptRes.push(gptRes);
-        const drawing: Doc[] = [];
-
-        strokeList.forEach((stroke: [InkData, string, string]) => {
-            const bounds = InkField.getBounds(stroke[0]);
-            const inkWidth = Math.min(5, ActiveInkWidth());
-            const inkDoc = Docs.Create.InkDocument(
-                stroke[0],
-                { title: 'stroke',
-                    x: bounds.left - inkWidth / 2,
-                    y: bounds.top - inkWidth / 2,
-                    _width: bounds.width + inkWidth,
-                    _height: bounds.height + inkWidth,
-                    stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
-                inkWidth,
-                opts.autoColor ? stroke[1] : ActiveInkColor(),
-                ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
-                ActiveArrowStart(),
-                ActiveArrowEnd(),
-                ActiveDash(),
-                ActiveIsInkMask()
-            );
-            drawing.push(inkDoc);
-        });
-
-        const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
-        if (collection) {
-            collection[DocData].freeform_fitContentsToBox = true;
-            Doc.AddDocToList(this._props.Document, 'data', collection);
-        }
+        drawing[DocData].freeform_fitContentsToBox = true;
+        Doc.AddDocToList(this._props.Document, 'data', drawing);
     };
 
     saveDrawing = async () => {
@@ -262,7 +232,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                                 icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
                                 iconPlacement="right"
                                 color={SettingsManager.userColor}
-                                onClick={this.generateDrawing}
+                                onClick={this.generateDrawings}
                             />
                         </div>
                         <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}>
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index c842551c3..52df598ee 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -7,17 +7,23 @@ import { ObservableReactComponent } from '../ObservableReactComponent';
 import { Button, IconButton } from 'browndash-components';
 import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
-import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
-import { InkData, InkTool } from '../../../fields/InkField';
+import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
+import { InkData, InkField, InkTool } from '../../../fields/InkField';
 import { SVGToBezier } from '../../util/bezierFit';
 const { parse } = require('svgson');
 import { Slider, Switch } from '@mui/material';
-import { Doc } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { DocumentView } from '../nodes/DocumentView';
-import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import './SmartDrawHandler.scss';
 import { unimplementedFunction } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { ImageField, URLField } from '../../../fields/URLField';
+import { CollectionCardView } from '../collections/CollectionCardDeckView';
+import { InkingStroke } from '../InkingStroke';
+import { undoBatch } from '../../util/UndoManager';
 
 export interface DrawingOptions {
     text: string;
@@ -46,13 +52,43 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
     @observable private _canInteract: boolean = true;
-    public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
-    public _deleteFunc: (doc?: Doc) => void = () => {};
+
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
     private _selectedDoc: Doc | undefined = undefined;
     private _errorOccurredOnce = false;
 
+    public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction;
+    public 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]);
+            const inkWidth = Math.min(5, ActiveInkWidth());
+            const inkDoc = Docs.Create.InkDocument(
+                stroke[0],
+                { title: 'stroke',
+                        x: bounds.left - inkWidth / 2,
+                        y: bounds.top - inkWidth / 2,
+                        _width: bounds.width + inkWidth,
+                        _height: bounds.height + inkWidth,
+                        stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+                inkWidth,
+                opts.autoColor ? stroke[1] : ActiveInkColor(),
+                ActiveInkBezierApprox(),
+                stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+                ActiveArrowStart(),
+                ActiveArrowEnd(),
+                ActiveDash(),
+                ActiveIsInkMask()
+            );
+            drawing.push(inkDoc);
+        });
+
+        const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+        return collection;
+    };
+    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
+
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -90,25 +126,26 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
 
     @action
-    displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+    setEdit = () => {
+        this._showEditBox = !this._showEditBox;
+    };
+
+    @action
+    displaySmartDrawHandler = (x: number, y: number) => {
         this._pageX = x;
         this._pageY = y;
         this._display = true;
-        this._addFunc = addFunc;
-        this._deleteFunc = deleteFunc;
     };
 
     @action
-    displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+    displayRegenerate = (x: number, y: number) => {
         this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
-        const docData = this._selectedDoc[DocData];
-        this._addFunc = addFunc;
-        this._deleteFunc = deleteFunc;
         this._pageX = x;
         this._pageY = y;
         this._display = false;
         this._showRegenerate = true;
         this._showEditBox = false;
+        const docData = this._selectedDoc[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 };
     };
@@ -155,8 +192,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             this._showOptions = false;
             try {
                 await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
-                this._showRegenerate = true;
                 this.hideSmartDrawHandler();
+                this._showRegenerate = true;
             } catch (err) {
                 if (this._errorOccurredOnce) {
                     console.error('GPT call failed', err);
@@ -181,16 +218,14 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             return;
         }
         console.log(res);
-        const strokeData = await this.parseResponse(res, startPt, false, autoColor);
+        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 strokeData;
     };
 
-    @action
-    edit = () => {
-        this._showEditBox = !this._showEditBox;
-    };
-
     @action
     regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
         if (lastInput) this._lastInput = lastInput;
@@ -211,14 +246,18 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 return;
             }
             console.log(res);
-            await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+            const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+            this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(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('GPT call failed', err);
+            console.error('Error regenerating drawing', err);
         }
     };
 
     @action
-    parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
+    parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
         const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
         if (svg) {
             this._lastResponse = svg[0];
@@ -235,16 +274,70 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                     (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
                 ]);
             });
-            if (regenerate) {
-                if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
-                this._addFunc(strokeData, this._lastInput, svg[0]);
-            } else {
-                this._addFunc(strokeData, this._lastInput, svg[0]);
-            }
             return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
         }
     };
 
+    colorWithGPT = async (drawing: Doc) => {
+        const img = await this.getIcon(drawing);
+        const { href } = (img as URLField).url;
+        const hrefParts = href.split('.');
+        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+        try {
+            const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
+            const strokes = DocListCast(drawing[DocData].data);
+            const coords: string[] = [];
+            strokes.forEach((stroke, i) => {
+                const inkingStroke = DocumentView.getDocumentView(stroke)?.ComponentView as InkingStroke;
+                const { inkData } = inkingStroke.inkScaledData();
+                coords.push(
+                    `${i + 1}. ${inkData
+                        .filter((point, index) => {
+                            return index % 4 === 0 || index == inkData.length - 1;
+                        })
+                        .map(point => {
+                            return `(${point.X.toString()}, ${point.Y.toString()})`;
+                        })}`
+                );
+            });
+            const response = await gptDrawingColor(hrefBase64, coords);
+            console.log(response);
+            const colorResponse = await gptAPICall(response, GPTCallType.COLOR, undefined);
+            console.log(colorResponse);
+            this.colorStrokes(colorResponse, drawing);
+        } catch (error) {
+            console.log('GPT call failed');
+        }
+    };
+
+    @undoBatch
+    colorStrokes = (res: string, drawing: Doc) => {
+        const colorList = res.match(/\{.*?\}/g);
+        const strokes = DocListCast(drawing[DocData].data);
+        colorList?.forEach((colors, index) => {
+            const strokeAndFill = colors.match(/#[0-9A-Fa-f]{6}/g);
+            if (strokeAndFill && strokeAndFill.length == 2) {
+                strokes[index][DocData].color = strokeAndFill[0];
+                const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
+                const { inkData } = inkStroke.inkScaledData();
+                if (InkingStroke.IsClosed(inkData)) {
+                    strokes[index][DocData].fillColor = strokeAndFill[1];
+                }
+            }
+        });
+    };
+
+    async getIcon(doc: Doc) {
+        const docView = DocumentView.getDocumentView(doc);
+        console.log(doc);
+        if (docView) {
+            console.log(docView);
+            docView.ComponentView?.updateIcon?.();
+            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+        }
+        return undefined;
+    }
+
     render() {
         if (this._display) {
             return (
@@ -397,7 +490,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                             color={SettingsManager.userColor}
                             onClick={this.handleSendClick}
                         />
-                        <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
+                        <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.setEdit} />
                         {this._showEditBox && (
                             <div
                                 style={{
-- 
cgit v1.2.3-70-g09d2


From 6f73686ec4dc3e01ae3eacc0150aa59eafea0325 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Tue, 27 Aug 2024 16:22:33 -0400
Subject: pulling from master

---
 eslint.config.mjs                                  |  17 +-
 package-lock.json                                  | 195 ++++++++++-----------
 package.json                                       |   8 +-
 src/client/util/DropConverter.ts                   |   4 +
 src/client/views/InkStrokeProperties.ts            |   7 +-
 src/client/views/MarqueeAnnotator.tsx              | 145 ---------------
 src/client/views/PropertiesView.scss               |   2 +-
 src/client/views/PropertiesView.tsx                |  52 +++---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  64 +++----
 .../collectionFreeForm/MarqueeOptionsMenu.tsx      |   2 -
 .../collections/collectionFreeForm/MarqueeView.tsx |  65 -------
 src/client/views/pdf/AnchorMenu.tsx                |  20 +--
 src/client/views/smartdraw/AnnotationPalette.tsx   |  13 ++
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  22 ++-
 14 files changed, 213 insertions(+), 403 deletions(-)

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

diff --git a/eslint.config.mjs b/eslint.config.mjs
index b35601abd..c69209327 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,6 +1,13 @@
-import pluginJs from '@eslint/js';
-import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
-import globals from 'globals';
-import tseslint from 'typescript-eslint';
+import globals from "globals";
+import pluginJs from "@eslint/js";
+import tseslint from "typescript-eslint";
+import pluginReact from "eslint-plugin-react";
 
-export default [{ languageOptions: { globals: { ...globals.browser, ...globals.node } } }, pluginJs.configs.recommended, ...tseslint.configs.recommended, pluginReactConfig];
+
+export default [
+  {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
+  {languageOptions: { globals: globals.browser }},
+  pluginJs.configs.recommended,
+  ...tseslint.configs.recommended,
+  pluginReact.configs.flat.recommended,
+];
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7f6237ef5..95fb8ce11 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -246,7 +246,7 @@
                 "xregexp": "^5.1.1"
             },
             "devDependencies": {
-                "@eslint/js": "^9.1.1",
+                "@eslint/js": "^9.9.1",
                 "@types/adm-zip": "^0.5.5",
                 "@types/animejs": "^3.1.12",
                 "@types/archiver": "^6.0.2",
@@ -301,9 +301,9 @@
                 "eslint-plugin-jsx-a11y": "^6.8.0",
                 "eslint-plugin-node": "^11.1.0",
                 "eslint-plugin-prettier": "^5.0.1",
-                "eslint-plugin-react": "^7.34.1",
+                "eslint-plugin-react": "^7.35.0",
                 "eslint-plugin-react-hooks": "^4.6.0",
-                "globals": "^15.1.0",
+                "globals": "^15.9.0",
                 "jsdom": "^24.0.0",
                 "mocha": "^10.2.0",
                 "prettier": "^3.1.0",
@@ -312,7 +312,7 @@
                 "ts-loader": "^9.5.1",
                 "ts-node": "^10.9.1",
                 "ts-node-dev": "^2.0.0",
-                "typescript-eslint": "^7.8.0",
+                "typescript-eslint": "^7.18.0",
                 "webpack-dev-server": "^5.0.4"
             },
             "engines": {
@@ -2637,10 +2637,11 @@
             }
         },
         "node_modules/@eslint/js": {
-            "version": "9.6.0",
-            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz",
-            "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==",
+            "version": "9.9.1",
+            "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
+            "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
             }
@@ -9946,16 +9947,17 @@
             "dev": true
         },
         "node_modules/@typescript-eslint/eslint-plugin": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
-            "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
+            "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@eslint-community/regexpp": "^4.10.0",
-                "@typescript-eslint/scope-manager": "7.16.0",
-                "@typescript-eslint/type-utils": "7.16.0",
-                "@typescript-eslint/utils": "7.16.0",
-                "@typescript-eslint/visitor-keys": "7.16.0",
+                "@typescript-eslint/scope-manager": "7.18.0",
+                "@typescript-eslint/type-utils": "7.18.0",
+                "@typescript-eslint/utils": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0",
                 "graphemer": "^1.4.0",
                 "ignore": "^5.3.1",
                 "natural-compare": "^1.4.0",
@@ -9979,14 +9981,15 @@
             }
         },
         "node_modules/@typescript-eslint/parser": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
-            "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
-            "dependencies": {
-                "@typescript-eslint/scope-manager": "7.16.0",
-                "@typescript-eslint/types": "7.16.0",
-                "@typescript-eslint/typescript-estree": "7.16.0",
-                "@typescript-eslint/visitor-keys": "7.16.0",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
+            "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
+            "license": "BSD-2-Clause",
+            "dependencies": {
+                "@typescript-eslint/scope-manager": "7.18.0",
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/typescript-estree": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0",
                 "debug": "^4.3.4"
             },
             "engines": {
@@ -10006,12 +10009,13 @@
             }
         },
         "node_modules/@typescript-eslint/scope-manager": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
-            "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
+            "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/types": "7.16.0",
-                "@typescript-eslint/visitor-keys": "7.16.0"
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0"
             },
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
@@ -10022,13 +10026,14 @@
             }
         },
         "node_modules/@typescript-eslint/type-utils": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
-            "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
+            "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/typescript-estree": "7.16.0",
-                "@typescript-eslint/utils": "7.16.0",
+                "@typescript-eslint/typescript-estree": "7.18.0",
+                "@typescript-eslint/utils": "7.18.0",
                 "debug": "^4.3.4",
                 "ts-api-utils": "^1.3.0"
             },
@@ -10049,9 +10054,10 @@
             }
         },
         "node_modules/@typescript-eslint/types": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
-            "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
+            "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
+            "license": "MIT",
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
             },
@@ -10061,12 +10067,13 @@
             }
         },
         "node_modules/@typescript-eslint/typescript-estree": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
-            "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
+            "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
+            "license": "BSD-2-Clause",
             "dependencies": {
-                "@typescript-eslint/types": "7.16.0",
-                "@typescript-eslint/visitor-keys": "7.16.0",
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/visitor-keys": "7.18.0",
                 "debug": "^4.3.4",
                 "globby": "^11.1.0",
                 "is-glob": "^4.0.3",
@@ -10088,9 +10095,10 @@
             }
         },
         "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
-            "version": "7.6.2",
-            "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
-            "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
+            "version": "7.6.3",
+            "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+            "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+            "license": "ISC",
             "bin": {
                 "semver": "bin/semver.js"
             },
@@ -10099,15 +10107,16 @@
             }
         },
         "node_modules/@typescript-eslint/utils": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
-            "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
+            "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.4.0",
-                "@typescript-eslint/scope-manager": "7.16.0",
-                "@typescript-eslint/types": "7.16.0",
-                "@typescript-eslint/typescript-estree": "7.16.0"
+                "@typescript-eslint/scope-manager": "7.18.0",
+                "@typescript-eslint/types": "7.18.0",
+                "@typescript-eslint/typescript-estree": "7.18.0"
             },
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
@@ -10121,11 +10130,12 @@
             }
         },
         "node_modules/@typescript-eslint/visitor-keys": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
-            "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
+            "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/types": "7.16.0",
+                "@typescript-eslint/types": "7.18.0",
                 "eslint-visitor-keys": "^3.4.3"
             },
             "engines": {
@@ -10891,18 +10901,6 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
-        "node_modules/array.prototype.toreversed": {
-            "version": "1.1.2",
-            "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz",
-            "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==",
-            "dev": true,
-            "dependencies": {
-                "call-bind": "^1.0.2",
-                "define-properties": "^1.2.0",
-                "es-abstract": "^1.22.1",
-                "es-shim-unscopables": "^1.0.0"
-            }
-        },
         "node_modules/array.prototype.tosorted": {
             "version": "1.1.4",
             "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz",
@@ -17072,6 +17070,7 @@
             "version": "3.0.1",
             "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
             "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+            "license": "MIT",
             "dependencies": {
                 "path-type": "^4.0.0"
             },
@@ -17618,6 +17617,7 @@
             "version": "8.57.0",
             "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
             "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
+            "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.6.1",
@@ -18884,35 +18884,36 @@
             }
         },
         "node_modules/eslint-plugin-react": {
-            "version": "7.34.3",
-            "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz",
-            "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==",
+            "version": "7.35.0",
+            "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz",
+            "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "array-includes": "^3.1.8",
                 "array.prototype.findlast": "^1.2.5",
                 "array.prototype.flatmap": "^1.3.2",
-                "array.prototype.toreversed": "^1.1.2",
                 "array.prototype.tosorted": "^1.1.4",
                 "doctrine": "^2.1.0",
                 "es-iterator-helpers": "^1.0.19",
                 "estraverse": "^5.3.0",
+                "hasown": "^2.0.2",
                 "jsx-ast-utils": "^2.4.1 || ^3.0.0",
                 "minimatch": "^3.1.2",
                 "object.entries": "^1.1.8",
                 "object.fromentries": "^2.0.8",
-                "object.hasown": "^1.1.4",
                 "object.values": "^1.2.0",
                 "prop-types": "^15.8.1",
                 "resolve": "^2.0.0-next.5",
                 "semver": "^6.3.1",
-                "string.prototype.matchall": "^4.0.11"
+                "string.prototype.matchall": "^4.0.11",
+                "string.prototype.repeat": "^1.0.0"
             },
             "engines": {
                 "node": ">=4"
             },
             "peerDependencies": {
-                "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+                "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
             }
         },
         "node_modules/eslint-plugin-react-hooks": {
@@ -20855,10 +20856,11 @@
             }
         },
         "node_modules/globals": {
-            "version": "15.8.0",
-            "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz",
-            "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==",
+            "version": "15.9.0",
+            "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
+            "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=18"
             },
@@ -20886,6 +20888,7 @@
             "version": "11.1.0",
             "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
             "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+            "license": "MIT",
             "dependencies": {
                 "array-union": "^2.1.0",
                 "dir-glob": "^3.0.1",
@@ -34178,23 +34181,6 @@
                 "node": ">= 0.4"
             }
         },
-        "node_modules/object.hasown": {
-            "version": "1.1.4",
-            "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz",
-            "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==",
-            "dev": true,
-            "dependencies": {
-                "define-properties": "^1.2.1",
-                "es-abstract": "^1.23.2",
-                "es-object-atoms": "^1.0.0"
-            },
-            "engines": {
-                "node": ">= 0.4"
-            },
-            "funding": {
-                "url": "https://github.com/sponsors/ljharb"
-            }
-        },
         "node_modules/object.values": {
             "version": "1.2.0",
             "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz",
@@ -38422,6 +38408,7 @@
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
             "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+            "license": "MIT",
             "engines": {
                 "node": ">=8"
             }
@@ -38983,6 +38970,17 @@
                 "url": "https://github.com/sponsors/ljharb"
             }
         },
+        "node_modules/string.prototype.repeat": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz",
+            "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
+            "dev": true,
+            "license": "MIT",
+            "dependencies": {
+                "define-properties": "^1.1.3",
+                "es-abstract": "^1.17.5"
+            }
+        },
         "node_modules/string.prototype.trim": {
             "version": "1.2.9",
             "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz",
@@ -40552,14 +40550,15 @@
             "integrity": "sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ=="
         },
         "node_modules/typescript-eslint": {
-            "version": "7.16.0",
-            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.16.0.tgz",
-            "integrity": "sha512-kaVRivQjOzuoCXU6+hLnjo3/baxyzWVO5GrnExkFzETRYJKVHYkrJglOu2OCm8Hi9RPDWX1PTNNTpU5KRV0+RA==",
+            "version": "7.18.0",
+            "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.18.0.tgz",
+            "integrity": "sha512-PonBkP603E3tt05lDkbOMyaxJjvKqQrXsnow72sVeOFINDE/qNmnnd+f9b4N+U7W6MXnnYyrhtmF2t08QWwUbA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
-                "@typescript-eslint/eslint-plugin": "7.16.0",
-                "@typescript-eslint/parser": "7.16.0",
-                "@typescript-eslint/utils": "7.16.0"
+                "@typescript-eslint/eslint-plugin": "7.18.0",
+                "@typescript-eslint/parser": "7.18.0",
+                "@typescript-eslint/utils": "7.18.0"
             },
             "engines": {
                 "node": "^18.18.0 || >=20.0.0"
diff --git a/package.json b/package.json
index 16b2841be..8753e995b 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
         "tsc": "tsc -t es5"
     },
     "devDependencies": {
-        "@eslint/js": "^9.1.1",
+        "@eslint/js": "^9.9.1",
         "@types/adm-zip": "^0.5.5",
         "@types/animejs": "^3.1.12",
         "@types/archiver": "^6.0.2",
@@ -78,9 +78,9 @@
         "eslint-plugin-jsx-a11y": "^6.8.0",
         "eslint-plugin-node": "^11.1.0",
         "eslint-plugin-prettier": "^5.0.1",
-        "eslint-plugin-react": "^7.34.1",
+        "eslint-plugin-react": "^7.35.0",
         "eslint-plugin-react-hooks": "^4.6.0",
-        "globals": "^15.1.0",
+        "globals": "^15.9.0",
         "jsdom": "^24.0.0",
         "mocha": "^10.2.0",
         "prettier": "^3.1.0",
@@ -89,7 +89,7 @@
         "ts-loader": "^9.5.1",
         "ts-node": "^10.9.1",
         "ts-node-dev": "^2.0.0",
-        "typescript-eslint": "^7.8.0",
+        "typescript-eslint": "^7.18.0",
         "webpack-dev-server": "^5.0.4"
     },
     "dependencies": {
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 3e26fefad..aa7268b61 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -87,6 +87,10 @@ export function makeUserTemplateButton(doc: Doc) {
     return dbox;
 }
 
+/**
+ * Similar to makeUserTemplateButton, but rather than creating a draggable button for the template, it takes in
+ * an ImageField that will display.
+ */
 export function makeUserTemplateImage(doc: Doc, image: ImageField) {
     const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
     if (layoutDoc.type !== DocumentType.FONTICON) {
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 35d628a4e..ffda126f4 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -491,6 +491,12 @@ export class InkStrokeProperties {
             return inkCopy;
         });
 
+    /**
+     * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and
+     * removing excess control points with the simplify-js package.
+     * @param inkDocs
+     * @param tolerance Determines how strong the smooth effect will be
+     */
     @undoBatch
     smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => {
         inkDocs.forEach(inkDoc => {
@@ -500,7 +506,6 @@ export class InkStrokeProperties {
 
             const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
             console.log(result);
-            let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
             if (result && (result.Name === 'line' ? result.Score > 0.92 : result.Score > 0.85)) {
                 switch (result.Name) {
                     case Gestures.Line:
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index f06f3efe0..3d0216625 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -28,7 +28,6 @@ export interface MarqueeAnnotatorProps {
     marqueeContainer: HTMLDivElement;
     docView: () => DocumentView;
     savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
-    savedTapes: () => ObservableMap<number, HTMLDivElement[]>;
     selectionText: () => string;
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
@@ -75,7 +74,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
                 backgroundColor: color,
                 annotationOn: this.props.Document,
                 title: 'Annotation on ' + this.props.Document.title,
-                a,
             });
             marqueeAnno.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
             marqueeAnno.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
@@ -130,139 +128,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         return textRegionAnno;
     };
 
-    // @undoBatch
-    // makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
-    //     const savedTapeMap = savedTapes?.values() && Array.from(savedTapes?.values()).length ? savedTapes : this.props.savedTapes();
-    //     if (savedTapeMap.size === 0) return undefined;
-    //     const tapes = Array.from(savedTapeMap.values())[0];
-    //     const doc = this.props.Document;
-    //     const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
-    //     if (tapes.length && (tapes[0] as any).marqueeing) {
-    //         const anno = tapes[0];
-    //         const containerOffset = this.props.containerOffset?.() || [0, 0];
-    //         const tape = Docs.Create.FreeformDocument([], {
-    //             onClick: isLinkButton ? FollowLinkScript() : undefined,
-    //             backgroundColor: color,
-    //             annotationOn: this.props.Document,
-    //             title: 'Tape on ' + this.props.Document.title,
-    //         });
-    //         tape.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
-    //         tape.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
-    //         tape._height = parseInt(anno.style.height || '0') / scale;
-    //         tape._width = parseInt(anno.style.width || '0') / scale;
-    //         anno.remove();
-    //         savedTapeMap.clear();
-    //         return tape;
-    //     }
-
-    // const textRegionAnno = Docs.Create.ConfigDocument({
-    //     annotationOn: this.props.Document,
-    //     text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
-    //     text_html: this.props.selectionText() as any,
-    //     backgroundColor: 'transparent',
-    //     presentation_duration: 2100,
-    //     presentation_transition: 500,
-    //     presentation_zoomText: true,
-    //     title: '>' + this.props.Document.title,
-    // });
-    // const textRegionAnnoProto = textRegionAnno[DocData];
-    // let minX = Number.MAX_VALUE;
-    // let maxX = -Number.MAX_VALUE;
-    // let minY = Number.MAX_VALUE;
-    // let maxY = -Number.MIN_VALUE;
-    // const annoRects: string[] = [];
-    // savedAnnoMap.forEach((value: HTMLDivElement[]) =>
-    //     value.forEach(anno => {
-    //         const x = parseInt(anno.style.left ?? '0');
-    //         const y = parseInt(anno.style.top ?? '0');
-    //         const height = parseInt(anno.style.height ?? '0');
-    //         const width = parseInt(anno.style.width ?? '0');
-    //         annoRects.push(`${x}:${y}:${width}:${height}`);
-    //         anno.remove();
-    //         minY = Math.min(NumCast(y), minY);
-    //         minX = Math.min(NumCast(x), minX);
-    //         maxY = Math.max(NumCast(y) + NumCast(height), maxY);
-    //         maxX = Math.max(NumCast(x) + NumCast(width), maxX);
-    //     })
-    // );
-
-    // textRegionAnnoProto.y = Math.max(minY, 0);
-    // textRegionAnnoProto.x = Math.max(minX, 0);
-    // textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
-    // textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
-    // textRegionAnnoProto.backgroundColor = color;
-    // // mainAnnoDocProto.text = this._selectionText;
-    // textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
-    // textRegionAnnoProto.opacity = 0;
-    // textRegionAnnoProto.layout_unrendered = true;
-    // savedAnnoMap.clear();
-    // return textRegionAnno;
-    // };
-
-    @undoBatch
-    makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
-        // const savedAnnoMap = savedTapes?.values() && Array.from(savedTapes?.values()).length ? savedTapes : this.props.savedTapes();
-        // if (savedAnnoMap.size === 0) return undefined;
-        // const savedAnnos = Array.from(savedAnnoMap.values())[0];
-        const doc = this.props.Document;
-        const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
-        const marqueeAnno = Docs.Create.FreeformDocument([], {
-            onClick: isLinkButton ? FollowLinkScript() : undefined,
-            backgroundColor: color,
-            annotationOn: this.props.Document,
-            title: 'Annotation on ' + this.props.Document.title,
-        });
-        marqueeAnno.x = NumCast(doc.freeform_panX_min) / scale;
-        marqueeAnno.y = NumCast(doc.freeform_panY_min) / scale;
-        marqueeAnno._height = parseInt('100') / scale;
-        marqueeAnno._width = parseInt('100') / scale;
-        return marqueeAnno;
-        // }
-
-        // const textRegionAnno = Docs.Create.ConfigDocument({
-        //     annotationOn: this.props.Document,
-        //     text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
-        //     text_html: this.props.selectionText() as any,
-        //     backgroundColor: 'transparent',
-        //     presentation_duration: 2100,
-        //     presentation_transition: 500,
-        //     presentation_zoomText: true,
-        //     title: '>' + this.props.Document.title,
-        // });
-        // const textRegionAnnoProto = textRegionAnno[DocData];
-        // let minX = Number.MAX_VALUE;
-        // let maxX = -Number.MAX_VALUE;
-        // let minY = Number.MAX_VALUE;
-        // let maxY = -Number.MIN_VALUE;
-        // const annoRects: string[] = [];
-        // savedAnnoMap.forEach((value: HTMLDivElement[]) =>
-        //     value.forEach(anno => {
-        //         const x = parseInt(anno.style.left ?? '0');
-        //         const y = parseInt(anno.style.top ?? '0');
-        //         const height = parseInt(anno.style.height ?? '0');
-        //         const width = parseInt(anno.style.width ?? '0');
-        //         annoRects.push(`${x}:${y}:${width}:${height}`);
-        //         anno.remove();
-        //         minY = Math.min(NumCast(y), minY);
-        //         minX = Math.min(NumCast(x), minX);
-        //         maxY = Math.max(NumCast(y) + NumCast(height), maxY);
-        //         maxX = Math.max(NumCast(x) + NumCast(width), maxX);
-        //     })
-        // );
-
-        // textRegionAnnoProto.y = Math.max(minY, 0);
-        // textRegionAnnoProto.x = Math.max(minX, 0);
-        // textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
-        // textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
-        // textRegionAnnoProto.backgroundColor = color;
-        // // mainAnnoDocProto.text = this._selectionText;
-        // textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
-        // textRegionAnnoProto.opacity = 0;
-        // textRegionAnnoProto.layout_unrendered = true;
-        // savedAnnoMap.clear();
-        // return textRegionAnno;
-    };
-
     @action
     highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
         // creates annotation documents for current highlights
@@ -272,15 +137,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         return annotationDoc as Doc;
     };
 
-    @action
-    tape = (color: string, isLinkButton: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
-        // creates annotation documents for current highlights
-        const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
-        const tape = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeTapeDocument(color, isLinkButton, savedTapes);
-        addAsAnnotation && tape && this.props.addDocument(tape);
-        return tape as Doc;
-    };
-
     public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => {
         div.style.backgroundColor = '#ACCEF7';
         div.style.opacity = '0.5';
@@ -327,7 +183,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         AnchorMenu.Instance.OnClick = undoable(() => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = (color: string) => this.highlight(color, false, undefined, true);
-        AnchorMenu.Instance.Tape = (color: string) => this.tape(color, false, undefined, true);
         AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]> /* , addAsAnnotation?: boolean */) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
         AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
 
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index aa825a6e9..a5e60b831 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -642,5 +642,5 @@
 .smooth,
 .color,
 .smooth-slider {
-    margin-top: 3px;
+    margin-top: 7px;
 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ac2625f32..c7b0a2ba5 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -802,7 +802,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     };
 
     getField(key: string) {
-        return this.containsInkDoc ? Field.toString(this.inkDoc?.[DocData][key] as FieldType) : Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
+        return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
     }
 
     @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
@@ -960,6 +960,22 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         const targetDoc = this.selectedLayoutDoc;
         return (
             <div>
+                {!targetDoc.layout_isSvg && (
+                    <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);
+                            }, 'smoothStrokes')}
+                        />
+                    </div>
+                )}
                 <div className="smooth">
                     <Toggle
                         text={'Smooth Ink Strokes'}
@@ -988,22 +1004,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         1
                     )}
                 </div>
-                {!targetDoc.layout_isSvg && (
-                    <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);
-                            }, 'smoothStrokes')}
-                        />
-                    </div>
-                )}
             </div>
         );
     }
@@ -1026,9 +1026,15 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get markScal() { return Number(this.getField('stroke_markerScale') || '1'); } // prettier-ignore
     set markScal(value) {
+        if (this.containsInkDoc) {
+            const childDocs = DocListCast(this.selectedDoc[DocData].data);
+            childDocs.forEach(doc => {
+                doc[DocData].stroke_markerScale = Number(value);
+            });
+        }
         this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
     }
-    @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '1'); } // prettier-ignore
+    @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '10'); } // prettier-ignore
     set smoothAmt(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value));
     }
@@ -1039,9 +1045,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             childDocs.forEach(doc => {
                 doc[DocData].stroke_startMarker = value;
             });
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
         }
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
     }
     @computed get markTail() {  return this.getField('stroke_endMarker') || ''; } // prettier-ignore
     set markTail(value) {
@@ -1050,9 +1055,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             childDocs.forEach(doc => {
                 doc[DocData].stroke_endMarker = value;
             });
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
         }
+        this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
     }
 
     regInput = (key: string, value: any, setter: (val: string) => {}) => (
@@ -1304,6 +1308,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         );
     }
 
+    /**
+     * Determines if a selected collection/group document contains any ink strokes to allow users to edit groups
+     * of ink strokes in the properties menu.
+     */
     containsInk = (selectedDoc: Doc) => {
         const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data);
         for (var i = 0; i < childDocs.length; i++) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8575807b3..8dc5f03b0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -599,6 +599,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     _eraserLock = 0;
     _eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch'
 
+    /**
+     * Erases strokes by intersecting them with an invisible "eraser stroke".
+     *  By default this iterates through all intersected ink strokes, determines which parts of a stroke need to be erased based on the type
+     * of eraser, draws back the ink segments to keep, and deletes the original stroke.
+     *
+     * Radius eraser: erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the
+     * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its
+     * intersection t-values are put into a map, which gets looped through to take out the erased parts.
+     */
     erase = (e: PointerEvent, delta: number[]) => {
         e.stopImmediatePropagation();
         const currPoint = { X: e.clientX, Y: e.clientY };
@@ -646,11 +655,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return false;
     };
 
-    /**
-     * Erases strokes by intersecting them with an invisible "eraser stroke".
-     *  By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
-     * and deletes the original stroke.
-     */
     @action
     onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
         this.erase(e, delta);
@@ -665,42 +669,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.erase(e, [0, 0]);
     };
 
-    /**
-     * Erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the
-     * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its
-     * intersection t-values are put into a map, which gets looped through to take out the erased parts.
-     * @param e
-     * @param down
-     * @param delta
-     * @returns
-     */
-    // @action
-    // onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
-    //     const currPoint = { X: e.clientX, Y: e.clientY };
-    //     this._eraserPts.push([currPoint.X, currPoint.Y]);
-    //     this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
-    //     const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
-
-    //     strokeMap.forEach((intersects, stroke) => {
-    //         if (!this._deleteList.includes(stroke)) {
-    //             this._deleteList.push(stroke);
-    //             SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
-    //             SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
-    //             const segments = this.radiusErase(stroke, intersects.sort());
-    //             segments?.forEach(segment =>
-    //                 this.forceStrokeGesture(
-    //                     e,
-    //                     Gestures.Stroke,
-    //                     segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
-    //                 )
-    //             );
-    //         }
-    //         stroke.layoutDoc.opacity = 0;
-    //         stroke.layoutDoc.dontIntersect = true;
-    //     });
-    //     return false;
-    // };
-
     forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
     };
@@ -1240,6 +1208,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return tVals;
     };
 
+    /**
+     * Creates an ink document to add to the freeform canvas.
+     */
     createInkDoc = (points: InkData, transformedBounds?: { x: number; y: number; width: number; height: number }) => {
         const bounds = InkField.getBounds(points);
         const B = transformedBounds || this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
@@ -1273,6 +1244,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     _drawing: Doc[] = [];
     _drawingContainer: Doc | undefined = undefined;
+    /**
+     * Function that creates a drawing--a group of ink strokes--to go with the smart draw function.
+     */
     @undoBatch
     createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
         this._drawing = [];
@@ -1304,6 +1278,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return collection;
     };
 
+    /**
+     * Part of regenerating a drawing--deletes the old drawing.
+     */
     removeDrawing = (doc?: Doc) => {
         this._batch = UndoManager.StartBatch('regenerateDrawing');
         if (doc) {
@@ -1317,6 +1294,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._drawing = [];
     };
 
+    /**
+     * Adds the created drawing to the freeform canvas and sets the metadata.
+     */
     addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
         const docData = doc[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
@@ -1926,8 +1906,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const locPt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
         this._eraserX = locPt[0];
         this._eraserY = locPt[1];
-        // Doc.ActiveTool === InkTool.RadiusEraser ? this._childPointerEvents = 'none' : this._childPointerEvents = 'all'
-        //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
 
     @action
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 76c37dff0..b3fdd9379 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -21,7 +21,6 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
     public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
     public classifyImages: (e: React.MouseEvent | undefined) => void = unimplementedFunction;
     public groupImages: () => void = unimplementedFunction;
-    public smoothStrokes: (docs?: Doc[]) => void = unimplementedFunction;
     public isShown = () => this._opacity > 0;
     constructor(props: any) {
         super(props);
@@ -42,7 +41,6 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
                 <IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
                 <IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
                 <IconButton tooltip="Classify Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
-                <IconButton tooltip="Smooth Strokes" onPointerDown={() => this.smoothStrokes} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
             </>
         );
         return this.getElement(buttons);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 92c0da983..bc1dfed92 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -284,7 +284,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView;
             MarqueeOptionsMenu.Instance.classifyImages = this.classifyImages;
             MarqueeOptionsMenu.Instance.groupImages = this.groupImages;
-            MarqueeOptionsMenu.Instance.smoothStrokes = this.smoothStrokes;
             document.addEventListener('pointerdown', hideMarquee, true);
             document.addEventListener('wheel', hideMarquee, true);
         } else {
@@ -497,70 +496,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this._props.Document.pivotField = 'data_label';
     });
 
-    @undoBatch
-    smoothStrokes = action((docs?: Doc[]) => {
-        docs && docs.length > 0 ? (this._selectedDocs = docs) : (this._selectedDocs = this.marqueeSelect(false, DocumentType.INK));
-        if (this._selectedDocs.length == 0) return;
-
-        this._selectedDocs.forEach(stroke => {
-            const docView = DocumentView.getDocumentView(stroke);
-            const inkStroke = docView?.ComponentView as InkingStroke;
-            const { inkData } = inkStroke.inkScaledData();
-
-            const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
-            console.log(result);
-            let polygonPoints: { X: number; Y: number }[] | undefined = undefined;
-            if (result && (result.Name === 'line' ? result.Score > 0.9 : result.Score > 0.8)) {
-                switch (result.Name) {
-                    case Gestures.Line:
-                    case Gestures.Triangle:
-                    case Gestures.Rectangle:
-                    case Gestures.Circle:
-                        GestureOverlay.makeBezierPolygon(inkData, result.Name, true);
-                        break;
-                    default:
-                }
-            } else {
-                const distances: number[] = [];
-                for (var i = 0; i < inkData.length - 3; i += 4) {
-                    distances.push(Math.sqrt((inkData[i].X - inkData[i + 3].X) ** 2 + (inkData[i].Y - inkData[i + 3].Y) ** 2));
-                }
-                const avgDist = (NumCast(stroke.width) + NumCast(stroke.height)) / 2;
-                // const avgDist = distances.reduce((a, b) => a + b) / distances.length;
-                if (Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) < avgDist) {
-                    inkData.pop();
-                    inkData.push({ X: inkData[0].X, Y: inkData[0].Y });
-                }
-                // const editedPoints: InkData = [];
-                // const toDelete: number[] = [];
-
-                // distances.forEach((dist, i) => {
-                //     if (dist < avgDist / 3) {
-                //         toDelete.unshift(i * 4);
-                //     }
-                // });
-                // toDelete.forEach(pt => {
-                //     InkStrokeProperties.Instance._currentPoint = pt;
-                //     docView && InkStrokeProperties.Instance.deletePoints(docView, false);
-                // });
-
-                // for (var i = 0; i < distances.length; i++) {
-                // if (distances[i] > avgDist / 3) {
-                //     editedPoints.push(...inkData.slice(i * 4, i * 4 + 4));
-                // } else {
-                //     if (i !== distances.length) {
-                //         editedPoints.push(...inkData.slice(i * 4, i * 4 + 2));
-                //         editedPoints.push(...inkData.slice(i * 4 + 6, i * 4 + 8));
-                //         i++;
-                //     }
-                // }
-                // }
-                // inkData.length = 0;
-                // inkData.push(...editedPoints);
-            }
-        });
-    });
-
     @undoBatch
     syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
         const selected = this.marqueeSelect(false);
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index ea574493a..7719f2f7c 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -57,7 +57,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
     public Highlight: (color: string) => Opt<Doc> = (/* color: string */) => undefined;
-    public Tape: (color: string) => Opt<Doc> = (/* color: string */) => undefined;
     public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = emptyFunction;
     public Delete: () => void = unimplementedFunction;
     public PinToPres: () => void = unimplementedFunction;
@@ -144,6 +143,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.addToCollection?.(newCol);
     };
 
+    /**
+     * Creates a GPT drawing based on selected text.
+     */
     gptDraw = async (e: React.PointerEvent) => {
         try {
             SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
@@ -155,6 +157,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         }
     };
 
+    /**
+     * Defines how a GPT drawing should be added to the current document.
+     */
     @undoBatch
     createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
         this.AddDrawingAnnotation(drawing);
@@ -203,12 +208,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         AnchorMenu.Instance.fadeOut(true);
     };
 
-    @action
-    tapeClicked = () => {
-        this.Tape(this.highlightColor);
-        // AnchorMenu.Instance.fadeOut(true);
-    };
-
     @computed get highlighter() {
         return (
             <Group>
@@ -219,13 +218,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                     colorPicker={this.highlightColor}
                     color={SettingsManager.userColor}
                 />
-                <IconButton
-                    tooltip="Click to Add Tape" //
-                    icon={<FontAwesomeIcon icon="tape" />}
-                    onClick={this.tapeClicked}
-                    colorPicker={this.highlightColor}
-                    color={SettingsManager.userColor}
-                />
                 <ColorPicker selectedColor={this.highlightColor} setFinalColor={this.changeHighlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} />
             </Group>
         );
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 7e4d46204..7f00fa2f2 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -107,6 +107,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
         this._props.Document[DocData].data = undefined;
     };
 
+    /**
+     * Adds a doc to the annotation palette. Gets a snapshot of the document to use as a preview in the palette. When this
+     * preview is dragged onto a parent document, a copy of that document is added as an annotation.
+     */
     public static addToPalette = async (doc: Doc) => {
         if (!doc.savedAsAnno) {
             const clone = await Doc.MakeClone(doc);
@@ -129,6 +133,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
         return undefined;
     }
 
+    /**
+     * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+     * the annotation palette.
+     */
     @undoBatch
     generateDrawings = action(async () => {
         this._isLoading = true;
@@ -159,6 +167,11 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
         Doc.AddDocToList(this._props.Document, 'data', drawing);
     };
 
+    /**
+     * Saves the currently showing, newly generated drawing to the annotation palette and sets the metadata.
+     * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user
+     * presses the "save drawing" button.
+     */
     saveDrawing = async () => {
         const cIndex: number = this._props.Document.carousel_index as number;
         const focusedDrawing = DocListCast(this._props.Document.data)[cIndex];
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 52df598ee..4ec787e07 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -208,6 +208,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         this._canInteract = true;
     };
 
+    /**
+     * Calls GPT API to create a drawing based on user input
+     */
     @action
     drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
         if (input === '') return;
@@ -226,6 +229,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         return strokeData;
     };
 
+    /**
+     * Regenerates drawings with the option to add a specific regenerate prompt/request.
+     */
     @action
     regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
         if (lastInput) this._lastInput = lastInput;
@@ -256,6 +262,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         }
     };
 
+    /**
+     * Parses the svg code that GPT returns into Bezier curves.
+     */
     @action
     parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
         const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
@@ -278,6 +287,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         }
     };
 
+    /**
+     * Sends request to GPT API to recolor a selected ink document or group of ink documents.
+     */
     colorWithGPT = async (drawing: Doc) => {
         const img = await this.getIcon(drawing);
         const { href } = (img as URLField).url;
@@ -310,6 +322,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         }
     };
 
+    /**
+     * Function that parses the GPT color response and sets the selected stroke(s) to the new color.
+     */
     @undoBatch
     colorStrokes = (res: string, drawing: Doc) => {
         const colorList = res.match(/\{.*?\}/g);
@@ -320,13 +335,14 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 strokes[index][DocData].color = strokeAndFill[0];
                 const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
                 const { inkData } = inkStroke.inkScaledData();
-                if (InkingStroke.IsClosed(inkData)) {
-                    strokes[index][DocData].fillColor = strokeAndFill[1];
-                }
+                InkingStroke.IsClosed(inkData) ? (strokes[index][DocData].fillColor = strokeAndFill[1]) : (strokes[index][DocData].fillColor = undefined);
             }
         });
     };
 
+    /**
+     * Gets an image snapshot of a doc. In this class, it's used to snapshot a selected ink stroke/group to use for GPT color.
+     */
     async getIcon(doc: Doc) {
         const docView = DocumentView.getDocumentView(doc);
         console.log(doc);
-- 
cgit v1.2.3-70-g09d2


From f7cdcb654e83d7fdbfd0b1cfc80c485bb9554f08 Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Wed, 28 Aug 2024 22:03:12 -0400
Subject: final changes, ready for pull request

---
 eslint.config.mjs                                | 67 +++++-------------------
 package-lock.json                                | 15 ++++--
 package.json                                     | 10 ++--
 src/client/util/Scripting.ts                     |  4 +-
 src/client/views/nodes/PDFBox.scss               | 16 +-----
 src/client/views/nodes/PDFBox.tsx                | 37 ++++---------
 src/client/views/smartdraw/AnnotationPalette.tsx | 14 +++--
 src/client/views/smartdraw/SmartDrawHandler.tsx  | 15 ++++--
 8 files changed, 59 insertions(+), 119 deletions(-)

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

diff --git a/eslint.config.mjs b/eslint.config.mjs
index 619966f20..12ad3300a 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,57 +1,14 @@
-import pluginJs from '@eslint/js';
-import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
-import globals from 'globals';
-import tseslint from 'typescript-eslint';
+import globals from "globals";
+import pluginJs from "@eslint/js";
+import tseslint from "typescript-eslint";
+import pluginReact from "eslint-plugin-react";
 
-export default [
-    {
-        languageOptions: { globals: { ...globals.browser, ...globals.node } },
-    },
-    pluginJs.configs.recommended,
-    ...tseslint.configs.recommended,
-    {
-        rules: {
-            'node/no-missing-import': 0,
-            'no-console': 'off',
-            'func-names': 'off',
-            'no-process-exit': 'off',
-            'object-shorthand': 'off',
-            'class-methods-use-this': 'off',
-            'single-quote': 'off',
-            'max-classes-per-file': 0,
-
-            'react/jsx-filename-extension': [
-                2,
-                {
-                    extensions: ['.js', '.jsx', '.ts', '.tsx'],
-                },
-            ],
 
-            'import/prefer-default-export': 'off',
-            'no-unused-expressions': 'off',
-            '@typescript-eslint/no-unused-expressions': 'off',
-            'prefer-template': 'off',
-            'no-inner-declarations': 'off',
-            'no-plusplus': 'off',
-            'no-multi-assign': 'off',
-            'no-underscore-dangle': 'off',
-            'no-nested-ternary': 'off',
-            'lines-between-class-members': 'off',
-            'no-shadow': 'off',
-            '@typescript-eslint/no-shadow': 'warn',
-            'no-unused-vars': 'off',
-            '@typescript-eslint/no-unused-vars': 'error',
-            '@typescript-eslint/no-namespace': 'off',
-            'react/destructuring-assignment': 0,
-            'prefer-arrow-callback': 'error',
-            'no-return-assign': 'error',
-            'no-await-in-loop': 'error',
-            'no-loop-func': 'error',
-            'no-cond-assign': 'error',
-            'no-use-before-define': 'error',
-            'no-explicit-any': 'error',
-            'no-restricted-globals': ['error', 'event'],
-        },
-    },
-    pluginReactConfig,
-];
+export default [
+  {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
+  {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
+  {languageOptions: { globals: globals.browser }},
+  pluginJs.configs.recommended,
+  ...tseslint.configs.recommended,
+  pluginReact.configs.flat.recommended,
+];
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 81f5976d6..c4d69bbcf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -249,7 +249,7 @@
                 "xregexp": "^5.1.1"
             },
             "devDependencies": {
-                "@eslint/js": "^9.1.1",
+                "@eslint/js": "^9.9.1",
                 "@types/adm-zip": "^0.5.5",
                 "@types/animejs": "^3.1.12",
                 "@types/archiver": "^6.0.2",
@@ -298,9 +298,9 @@
                 "@types/youtube": "^0.1.0",
                 "chai": "^5.0.0",
                 "cross-env": "^7.0.3",
-                "eslint": "^9.9.0",
-                "eslint-plugin-react": "^7.34.1",
-                "globals": "^15.1.0",
+                "eslint": "^9.9.1",
+                "eslint-plugin-react": "^7.35.0",
+                "globals": "^15.9.0",
                 "jsdom": "^25.0.0",
                 "mocha": "^10.2.0",
                 "prettier": "^3.1.0",
@@ -309,7 +309,7 @@
                 "ts-loader": "^9.5.1",
                 "ts-node": "^10.9.1",
                 "ts-node-dev": "^2.0.0",
-                "typescript-eslint": "^8.2.0",
+                "typescript-eslint": "^8.3.0",
                 "webpack-dev-server": "^5.0.4"
             },
             "engines": {
@@ -2681,6 +2681,7 @@
             "version": "9.9.1",
             "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
             "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+            "license": "MIT",
             "engines": {
                 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
             }
@@ -17478,6 +17479,7 @@
             "version": "9.9.1",
             "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
             "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
+            "license": "MIT",
             "dependencies": {
                 "@eslint-community/eslint-utils": "^4.2.0",
                 "@eslint-community/regexpp": "^4.11.0",
@@ -17537,6 +17539,7 @@
             "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz",
             "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "array-includes": "^3.1.8",
                 "array.prototype.findlast": "^1.2.5",
@@ -19332,6 +19335,7 @@
             "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
             "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
             "dev": true,
+            "license": "MIT",
             "engines": {
                 "node": ">=18"
             },
@@ -32652,6 +32656,7 @@
             "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.3.0.tgz",
             "integrity": "sha512-EvWjwWLwwKDIJuBjk2I6UkV8KEQcwZ0VM10nR1rIunRDIP67QJTZAHBXTX0HW/oI1H10YESF8yWie8fRQxjvFA==",
             "dev": true,
+            "license": "MIT",
             "dependencies": {
                 "@typescript-eslint/eslint-plugin": "8.3.0",
                 "@typescript-eslint/parser": "8.3.0",
diff --git a/package.json b/package.json
index 0b17d9b60..51e7cf8a9 100644
--- a/package.json
+++ b/package.json
@@ -23,7 +23,7 @@
         "tsc": "tsc -t es5"
     },
     "devDependencies": {
-        "@eslint/js": "^9.1.1",
+        "@eslint/js": "^9.9.1",
         "@types/adm-zip": "^0.5.5",
         "@types/animejs": "^3.1.12",
         "@types/archiver": "^6.0.2",
@@ -72,9 +72,9 @@
         "@types/youtube": "^0.1.0",
         "chai": "^5.0.0",
         "cross-env": "^7.0.3",
-        "eslint": "^9.9.0",
-        "eslint-plugin-react": "^7.34.1",
-        "globals": "^15.1.0",
+        "eslint": "^9.9.1",
+        "eslint-plugin-react": "^7.35.0",
+        "globals": "^15.9.0",
         "jsdom": "^25.0.0",
         "mocha": "^10.2.0",
         "prettier": "^3.1.0",
@@ -83,7 +83,7 @@
         "ts-loader": "^9.5.1",
         "ts-node": "^10.9.1",
         "ts-node-dev": "^2.0.0",
-        "typescript-eslint": "^8.2.0",
+        "typescript-eslint": "^8.3.0",
         "webpack-dev-server": "^5.0.4"
     },
     "dependencies": {
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c63d3d7cb..cb314e3f1 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,7 +1,7 @@
 // export const ts = (window as any).ts;
 // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
 // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
-import typescriptlib from 'type_decls.d';
+// import typescriptlib from 'type_decls.d';
 import * as ts from 'typescript';
 import { Doc, FieldType } from '../../fields/Doc';
 import { RefField } from '../../fields/RefField';
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
     host.writeFile('file.ts', funcScript);
 
-    if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+    // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
     const program = ts.createProgram(['file.ts'], {}, host);
     const testResult = program.emit();
     const outputText = host.readFile('file.js');
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 6e24b2931..7bca1230f 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -20,27 +20,15 @@
         top: 0;
         left: 0;
 
-        .pdfBox-sidebarBtn-container {
-            display: flex;
-            flex-direction: row;
-            position: absolute;
-            width: 53px;
-            height: 33px;
-            right: 5px;
-            align-items: center;
-            justify-content: space-between;
-            z-index: 1;
-        }
-
         // glr: This should really be the same component as text and PDFs
         .pdfBox-sidebarBtn {
             background: $black;
             height: 25px;
             width: 25px;
-            // right: 5px;
+            right: 5px;
             color: $white;
             display: flex;
-            // position: absolute;
+            position: absolute;
             align-items: center;
             justify-content: center;
             border-radius: 3px;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 08f14193a..b17275a1e 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,6 +1,4 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { IconButton } from 'browndash-components';
-import { black } from 'colors';
 import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as Pdfjs from 'pdfjs-dist';
@@ -505,30 +503,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
     @computed get sidebarHandle() {
         return (
-            <div className="pdfBox-sidebarBtn-container">
-                <div
-                    className="pdfBox-sidebarBtn"
-                    key="sidebar"
-                    title="Toggle Sidebar"
-                    style={{
-                        display: !this._props.isContentActive() ? 'none' : undefined,
-                        top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
-                        backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
-                    }}>
-                    {/* // onPointerDown={e => this.sidebarBtnDown(e, true)} */}
-                    <IconButton tooltip="Toggle Annotation Palette" icon={<FontAwesomeIcon style={{ color: Colors.WHITE }} icon="palette" />} onPointerDown={e => this.sidebarBtnDown(e, true)} />
-                </div>
-                <div
-                    className="pdfBox-sidebarBtn"
-                    key="sidebar"
-                    title="Toggle Sidebar"
-                    style={{
-                        display: !this._props.isContentActive() ? 'none' : undefined,
-                        top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
-                        backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
-                    }}>
-                    <IconButton tooltip="Toggle Sidebar" icon={<FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />} onPointerDown={e => this.sidebarBtnDown(e, true)} />
-                </div>
+            <div
+                className="pdfBox-sidebarBtn"
+                key="sidebar"
+                title="Toggle Sidebar"
+                style={{
+                    display: !this._props.isContentActive() ? 'none' : undefined,
+                    top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
+                    backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+                }}
+                onPointerDown={e => this.sidebarBtnDown(e, true)}>
+                <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
             </div>
         );
     }
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index b57907152..c296138a8 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -1,4 +1,3 @@
-import { faLaptopHouse } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Slider, Switch } from '@mui/material';
 import { Button } from 'browndash-components';
@@ -16,9 +15,8 @@ import { Docs } from '../../documents/Documents';
 import { makeUserTemplateImage } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
-import { undoable, undoBatch } from '../../util/UndoManager';
-import { CollectionFreeFormView, MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { undoBatch } from '../../util/UndoManager';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { DefaultStyleProvider } from '../StyleProvider';
@@ -26,7 +24,6 @@ import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageField } from '../../../fields/URLField';
-import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView';
 import { Copy } from '../../../fields/FieldSymbols';
 
 interface AnnotationPaletteProps {
@@ -45,7 +42,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
     private _gptRes: string[] = [];
 
-    constructor(props: any) {
+    constructor(props: AnnotationPaletteProps) {
         super(props);
         makeObservable(this);
     }
@@ -140,7 +137,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
     generateDrawings = action(async () => {
         this._isLoading = true;
         this._props.Document[DocData].data = undefined;
-        for (var i = 0; i < 3; i++) {
+        for (let i = 0; i < 3; i++) {
             try {
                 SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
                 this._canInteract = false;
@@ -150,9 +147,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                     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');
+                console.log('Error generating drawing', e);
             }
         }
+        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
         this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
         this._userInput = '';
         this._isLoading = false;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 4ec787e07..368970f75 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -10,7 +10,7 @@ import { AiOutlineSend } from 'react-icons/ai';
 import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
 import { InkData, InkField, InkTool } from '../../../fields/InkField';
 import { SVGToBezier } from '../../util/bezierFit';
-const { parse } = require('svgson');
+import { INode, parse } from 'svgson';
 import { Slider, Switch } from '@mui/material';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
@@ -35,6 +35,7 @@ export interface DrawingOptions {
 }
 
 @observer
+// eslint-disable-next-line @typescript-eslint/no-empty-object-type
 export class SmartDrawHandler extends ObservableReactComponent<{}> {
     static Instance: SmartDrawHandler;
 
@@ -59,7 +60,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     private _errorOccurredOnce = false;
 
     public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction;
-    public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+    public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
         const drawing: Doc[] = [];
         strokeList.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
@@ -89,6 +90,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     };
     public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
 
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -223,6 +225,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         console.log(res);
         const strokeData = await this.parseSvg(res, startPt, false, autoColor);
         const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
         drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
 
         this._errorOccurredOnce = false;
@@ -253,8 +256,10 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             }
             console.log(res);
             const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
             this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(this._selectedDoc);
             const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
             drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
             return strokeData;
         } catch (err) {
@@ -271,8 +276,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
         if (svg) {
             this._lastResponse = svg[0];
             const svgObject = await parse(svg[0]);
-            const svgStrokes: any = svgObject.children;
+            const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
+            // eslint-disable-next-line @typescript-eslint/no-explicit-any
             svgStrokes.forEach((child: any) => {
                 const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
                 strokeData.push([
@@ -318,7 +324,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             console.log(colorResponse);
             this.colorStrokes(colorResponse, drawing);
         } catch (error) {
-            console.log('GPT call failed');
+            console.log('GPT call failed', error);
         }
     };
 
@@ -335,6 +341,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 strokes[index][DocData].color = strokeAndFill[0];
                 const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
                 const { inkData } = inkStroke.inkScaledData();
+                // eslint-disable-next-line @typescript-eslint/no-unused-expressions
                 InkingStroke.IsClosed(inkData) ? (strokes[index][DocData].fillColor = strokeAndFill[1]) : (strokes[index][DocData].fillColor = undefined);
             }
         });
-- 
cgit v1.2.3-70-g09d2


From 0ac79ba6a7ab19b4aafbc11dac9bab4781d4bd40 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Aug 2024 13:07:53 -0400
Subject: merge cleanup cleanup

---
 eslint.config.mjs                                  |  67 +++++-
 src/ClientUtils.ts                                 |   9 +-
 src/client/cognitive_services/CognitiveServices.ts |  15 +-
 src/client/util/Scripting.ts                       |   4 +-
 src/client/util/SettingsManager.tsx                |   6 +-
 src/client/util/bezierFit.ts                       |  18 +-
 src/client/views/DocumentButtonBar.tsx             |  10 +-
 src/client/views/GestureOverlay.tsx                |  10 +-
 src/client/views/InkStrokeProperties.ts            | 268 +++++++++++----------
 src/client/views/PropertiesButtons.tsx             |   4 +-
 .../collections/CollectionStackedTimeline.tsx      |   2 +-
 .../views/collections/CollectionTreeView.tsx       |   4 -
 src/client/views/collections/TreeView.tsx          |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  22 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 src/client/views/pdf/Annotation.tsx                |   1 +
 src/client/views/pdf/PDFViewer.tsx                 |   4 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   |  20 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |   2 +-
 20 files changed, 256 insertions(+), 230 deletions(-)

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

diff --git a/eslint.config.mjs b/eslint.config.mjs
index 12ad3300a..619966f20 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,14 +1,57 @@
-import globals from "globals";
-import pluginJs from "@eslint/js";
-import tseslint from "typescript-eslint";
-import pluginReact from "eslint-plugin-react";
-
+import pluginJs from '@eslint/js';
+import pluginReactConfig from 'eslint-plugin-react/configs/recommended.js';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
 
 export default [
-  {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
-  {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}},
-  {languageOptions: { globals: globals.browser }},
-  pluginJs.configs.recommended,
-  ...tseslint.configs.recommended,
-  pluginReact.configs.flat.recommended,
-];
\ No newline at end of file
+    {
+        languageOptions: { globals: { ...globals.browser, ...globals.node } },
+    },
+    pluginJs.configs.recommended,
+    ...tseslint.configs.recommended,
+    {
+        rules: {
+            'node/no-missing-import': 0,
+            'no-console': 'off',
+            'func-names': 'off',
+            'no-process-exit': 'off',
+            'object-shorthand': 'off',
+            'class-methods-use-this': 'off',
+            'single-quote': 'off',
+            'max-classes-per-file': 0,
+
+            'react/jsx-filename-extension': [
+                2,
+                {
+                    extensions: ['.js', '.jsx', '.ts', '.tsx'],
+                },
+            ],
+
+            'import/prefer-default-export': 'off',
+            'no-unused-expressions': 'off',
+            '@typescript-eslint/no-unused-expressions': 'off',
+            'prefer-template': 'off',
+            'no-inner-declarations': 'off',
+            'no-plusplus': 'off',
+            'no-multi-assign': 'off',
+            'no-underscore-dangle': 'off',
+            'no-nested-ternary': 'off',
+            'lines-between-class-members': 'off',
+            'no-shadow': 'off',
+            '@typescript-eslint/no-shadow': 'warn',
+            'no-unused-vars': 'off',
+            '@typescript-eslint/no-unused-vars': 'error',
+            '@typescript-eslint/no-namespace': 'off',
+            'react/destructuring-assignment': 0,
+            'prefer-arrow-callback': 'error',
+            'no-return-assign': 'error',
+            'no-await-in-loop': 'error',
+            'no-loop-func': 'error',
+            'no-cond-assign': 'error',
+            'no-use-before-define': 'error',
+            'no-explicit-any': 'error',
+            'no-restricted-globals': ['error', 'event'],
+        },
+    },
+    pluginReactConfig,
+];
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index dc52218c5..55801df81 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -82,10 +82,6 @@ export function returnEmptyFilter() {
     return [] as string[];
 }
 
-export function returnEmptyDoclist() {
-    return [] as any[];
-}
-
 export namespace ClientUtils {
     export const CLICK_TIME = 300;
     export const DRAG_THRESHOLD = 4;
@@ -449,11 +445,10 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement |
     animateScroll();
 }
 
-export function addStyleSheet(styleType: string = 'text/css') {
+export function addStyleSheet() {
     const style = document.createElement('style');
-    style.type = styleType;
     const sheets = document.head.appendChild(style);
-    return (sheets as any).sheet;
+    return sheets.sheet;
 }
 export function addStyleSheetRule(sheet: CSSStyleSheet | null, selector: string, css: string | { [key: string]: string }, selectorPrefix = '.') {
     const propText =
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9f7701c54..3ee61cbfb 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -46,15 +46,14 @@ export enum Confidence {
 export namespace CognitiveServices {
     const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
         const apiKey = process.env[service.toUpperCase()];
-        if (apiKey) {
-            console.log(data);
+        if (!apiKey) {
             console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`);
             return undefined;
         }
 
         let results: any;
         try {
-            results = await manager.requester('has', manager.converter(data), service).then(json => JSON.parse(json));
+            results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
         } catch (e) {
             throw e;
         }
@@ -138,14 +137,6 @@ export namespace CognitiveServices {
                     points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(','),
                     language: 'en-US',
                 }));
-                console.log(
-                    JSON.stringify({
-                        version: 1,
-                        language: 'en-US',
-                        unit: 'mm',
-                        strokes,
-                    })
-                );
                 return JSON.stringify({
                     version: 1,
                     language: 'en-US',
@@ -345,7 +336,7 @@ export namespace CognitiveServices {
                         'Ocp-Apim-Subscription-Key': apiKey,
                     },
                 };
-                return request.post(options);
+                return rp.post(options);
             },
         };
 
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index cb314e3f1..c63d3d7cb 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,7 +1,7 @@
 // export const ts = (window as any).ts;
 // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
 // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
-// import typescriptlib from 'type_decls.d';
+import typescriptlib from 'type_decls.d';
 import * as ts from 'typescript';
 import { Doc, FieldType } from '../../fields/Doc';
 import { RefField } from '../../fields/RefField';
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
     host.writeFile('file.ts', funcScript);
 
-    // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+    if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
     const program = ts.createProgram(['file.ts'], {}, host);
     const testResult = program.emit();
     const outputText = host.readFile('file.js');
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 9e49117a7..9200d68db 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -27,7 +27,7 @@ export enum ColorScheme {
 }
 
 @observer
-export class SettingsManager extends React.Component<{}> {
+export class SettingsManager extends React.Component<object> {
     // eslint-disable-next-line no-use-before-define
     public static Instance: SettingsManager;
     static _settingsStyle = addStyleSheet();
@@ -85,7 +85,7 @@ export class SettingsManager extends React.Component<{}> {
             if (this._playgroundMode) {
                 DocServer.Control.makeReadOnly();
                 addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
-            } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable();
+            } else if (ClientUtils.CurrentUserEmail() !== 'guest') DocServer.Control.makeEditable();
         }),
         'set playgorund mode'
     );
@@ -121,7 +121,7 @@ export class SettingsManager extends React.Component<{}> {
         'change color scheme'
     );
 
-    constructor(props: {}) {
+    constructor(props: object) {
         super(props);
         makeObservable(this);
         SettingsManager.Instance = this;
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 693676bc3..4c7f4a0ba 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -2,7 +2,6 @@
 /* eslint-disable prefer-destructuring */
 /* eslint-disable no-param-reassign */
 /* eslint-disable camelcase */
-import e from 'cors';
 import { Point } from '../../pen-gestures/ndollar';
 
 export enum SVGType {
@@ -628,7 +627,7 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
 
 export function SVGToBezier(name: SVGType, attributes: any): Point[] {
     switch (name) {
-        case 'line':
+        case 'line': {
             const x1 = parseInt(attributes.x1);
             const x2 = parseInt(attributes.x2);
             const y1 = parseInt(attributes.y1);
@@ -639,8 +638,9 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 { X: x2, Y: y2 },
                 { X: x2, Y: y2 },
             ];
+        }
         case 'circle':
-        case 'ellipse':
+        case 'ellipse': {
             const c = 0.551915024494;
             const centerX = parseInt(attributes.cx);
             const centerY = parseInt(attributes.cy);
@@ -664,7 +664,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 { X: centerX - c * radiusX, Y: centerY + radiusY },
                 { X: centerX, Y: centerY + radiusY },
             ];
-        case 'rect':
+        }
+        case 'rect': {
             const x = parseInt(attributes.x);
             const y = parseInt(attributes.y);
             const width = parseInt(attributes.width);
@@ -687,14 +688,15 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 { X: x, Y: y },
                 { X: x, Y: y },
             ];
-        case 'path':
+        }
+        case 'path': {
             const coordList: Point[] = [];
             const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/);
             coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
             const matches: RegExpMatchArray[] = Array.from(
                 attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
             );
-            let lastPt: Point;
+            let lastPt: Point = { X: 0, Y: 0 };
             matches.forEach(match => {
                 if (match[0].startsWith('Q')) {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
@@ -725,7 +727,8 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                 coordList.pop();
             }
             return coordList;
-        case 'polygon':
+        }
+        case 'polygon': {
             const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
             let list: Point[] = [];
             coords.forEach(coord => {
@@ -737,6 +740,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             const firstPts = list.splice(0, 2);
             list = list.concat(firstPts);
             return list;
+        }
         default:
             return [];
     }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 8aa4c2093..096f058ad 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -9,7 +9,7 @@ import * as React from 'react';
 import { FaEdit } from 'react-icons/fa';
 import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
 import { emptyFunction } from '../../Utils';
-import { Doc, DocListCast } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
 import { Cast, DocCast } from '../../fields/Types';
 import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
 import { CalendarManager } from '../util/CalendarManager';
@@ -17,7 +17,7 @@ import { DictationManager } from '../util/DictationManager';
 import { DragManager } from '../util/DragManager';
 import { dropActionType } from '../util/DropActionTypes';
 import { SharingManager } from '../util/SharingManager';
-import { UndoManager, undoable, undoBatch } from '../util/UndoManager';
+import { UndoManager, undoable } from '../util/UndoManager';
 import './DocumentButtonBar.scss';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { PinProps } from './PinFuncs';
@@ -29,7 +29,6 @@ import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { AnnotationPalette } from './smartdraw/AnnotationPalette';
-import { DocData } from '../../fields/DocSymbols';
 
 @observer
 export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: unknown }> {
@@ -240,10 +239,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
-    @undoBatch
-    saveAnno = action(async (targetDoc: Doc) => {
-        await AnnotationPalette.addToPalette(targetDoc);
-    });
+    saveAnno = undoable(async (targetDoc: Doc) => await AnnotationPalette.addToPalette(targetDoc), 'save to palette');
 
     @computed
     get saveAnnoButton() {
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 63b472faf..befd19f6e 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -129,7 +129,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
     }
     @action
     onPointerUp = () => {
-        console.log('pointer up');
         DocumentView.DownDocView = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
@@ -145,9 +144,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
             else {
                 // need to decide when to turn gestures back on
                 const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]);
-                console.log(points);
                 let actionPerformed = false;
-                console.log(result);
                 if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
                     switch (result.Name) {
                         case Gestures.Line:
@@ -156,15 +153,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
                         case Gestures.Circle:
                             GestureOverlay.makeBezierPolygon(this._points, result.Name, true);
                             actionPerformed = this.dispatchGesture(result.Name);
-                            console.log(result.Name);
-                            console.log();
                             break;
                         case Gestures.Scribble:
                             console.log('scribble');
                             break;
                         case Gestures.RightAngle:
                             console.log('RightAngle');
-                        default:
+                            break;
+                        default: {
+                            /* empty */
+                        }
                     }
                 }
 
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index ffda126f4..b5cd72fa7 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -9,7 +9,7 @@ import { Cast, NumCast, toList } from '../../fields/Types';
 import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, undoable } from '../util/UndoManager';
 import { FitOneCurve } from '../util/bezierFit';
 import { InkingStroke } from './InkingStroke';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
@@ -92,8 +92,7 @@ export class InkStrokeProperties {
      * @param i index of first control point of segment being split
      * @param control The list of all control points of the ink.
      */
-    @undoBatch
-    addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
+    addPoints = undoable((inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
         this.applyFunction(inkView, (view: DocumentView /* , ink: InkData */) => {
             const doc = view.Document;
             const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
@@ -109,7 +108,7 @@ export class InkStrokeProperties {
 
             return controls;
         });
-    };
+    }, 'add ink points');
 
     /**
      * Scales a handle point of a control point that is adjacent to a newly added one.
@@ -164,46 +163,48 @@ export class InkStrokeProperties {
     /**
      * Deletes the current control point of the selected ink instance.
      */
-    @undoBatch
-    deletePoints = (inkView: DocumentView, preserve: boolean) =>
-        this.applyFunction(
-            inkView,
-            (view: DocumentView, ink: InkData) => {
-                const doc = view.Document;
-                const newPoints = ink.slice();
-                const brokenIndices = NumListCast(doc.brokenInkIndices);
-                if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
-                    newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
-                } else {
-                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
-                    const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-                    const samples: Point[] = [];
-                    let startDir = { x: 0, y: 0 };
-                    let endDir = { x: 0, y: 0 };
-                    for (let i = 0; i < splicedPoints.length / 4; i++) {
-                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                        if (i === 0) startDir = bez.derivative(0);
-                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
-                        for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
-                            const pt = bez.compute(t);
-                            samples.push(new Point(pt.x, pt.y));
-                        }
-                    }
-                    const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                    if (error < 100) {
-                        newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+    deletePoints = undoable(
+        (inkView: DocumentView, preserve: boolean) =>
+            this.applyFunction(
+                inkView,
+                (view: DocumentView, ink: InkData) => {
+                    const doc = view.Document;
+                    const newPoints = ink.slice();
+                    const brokenIndices = NumListCast(doc.brokenInkIndices);
+                    if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
+                        newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
                     } else {
-                        newPoints.splice(this._currentPoint - 2, 4);
+                        const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                        const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                        const samples: Point[] = [];
+                        let startDir = { x: 0, y: 0 };
+                        let endDir = { x: 0, y: 0 };
+                        for (let i = 0; i < splicedPoints.length / 4; i++) {
+                            const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                            if (i === 0) startDir = bez.derivative(0);
+                            if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+                            for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+                                const pt = bez.compute(t);
+                                samples.push(new Point(pt.x, pt.y));
+                            }
+                        }
+                        const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                        if (error < 100) {
+                            newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                        } else {
+                            newPoints.splice(this._currentPoint - 2, 4);
+                        }
                     }
-                }
-                doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
-                runInAction(() => {
-                    this._currentPoint = -1;
-                });
-                return newPoints.length < 4 ? undefined : newPoints;
-            },
-            true
-        );
+                    doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
+                    runInAction(() => {
+                        this._currentPoint = -1;
+                    });
+                    return newPoints.length < 4 ? undefined : newPoints;
+                },
+                true
+            ),
+        'delete ink points'
+    );
 
     /**
      * Rotates ink stroke(s) about a point
@@ -211,8 +212,7 @@ export class InkStrokeProperties {
      * @param angle The angle at which to rotate the ink in radians.
      * @param scrpt The center point of the rotation in screen coordinates
      */
-    @undoBatch
-    rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
+    rotateInk = undoable((inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
         this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
             const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
             return !inkCenterPt
@@ -224,7 +224,7 @@ export class InkStrokeProperties {
                       return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
                   });
         });
-    };
+    }, 'rotate ink');
 
     /**
      * Rotates ink stroke(s) about a point
@@ -232,8 +232,7 @@ export class InkStrokeProperties {
      * @param angle The angle at which to rotate the ink in radians.
      * @param scrpt The center point of the rotation in screen coordinates
      */
-    @undoBatch
-    stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
+    stretchInk = undoable((inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
         this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData) => {
             const ptFromScreen = view.ComponentView?.ptFromScreen;
             const ptToScreen = view.ComponentView?.ptToScreen;
@@ -247,77 +246,79 @@ export class InkStrokeProperties {
                       return ptFromScreen(newscrpt);
                   });
         });
-    };
+    }, 'stretch ink');
 
     /**
      * Handles the movement/scaling of a control point.
      */
-    @undoBatch
-    moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
-        inkView &&
-        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
-            const order = controlIndex % 4;
-            const closed = InkingStroke.IsClosed(ink);
-            const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
-            if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
-                const cptBefore = ink[controlIndex];
-                const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
-                const newink = origInk.slice();
-                const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
-                const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-                const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
-                if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
-                const samplesLeft: Point[] = [];
-                const samplesRight: Point[] = [];
-                let startDir = { x: 0, y: 0 };
-                let endDir = { x: 0, y: 0 };
-                for (let i = 0; i < nearestSeg / 4 + 1; i++) {
-                    const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                    if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
-                    if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
-                    for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
-                        const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
-                        samplesLeft.push(new Point(pt.x, pt.y));
+    moveControlPtHandle = undoable(
+        (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
+            inkView &&
+            this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+                const order = controlIndex % 4;
+                const closed = InkingStroke.IsClosed(ink);
+                const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
+                if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
+                    const cptBefore = ink[controlIndex];
+                    const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
+                    const newink = origInk.slice();
+                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                    const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                    const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
+                    if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
+                    const samplesLeft: Point[] = [];
+                    const samplesRight: Point[] = [];
+                    let startDir = { x: 0, y: 0 };
+                    let endDir = { x: 0, y: 0 };
+                    for (let i = 0; i < nearestSeg / 4 + 1; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
+                        if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+                        for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
+                            const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
+                            samplesLeft.push(new Point(pt.x, pt.y));
+                        }
                     }
-                }
-                let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
-                    const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                    if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
-                    if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
-                    for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
-                        const pt = bez.compute(Math.min(1, t));
-                        samplesRight.push(new Point(pt.x, pt.y));
+                    let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
+                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
+                        for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+                            const pt = bez.compute(Math.min(1, t));
+                            samplesRight.push(new Point(pt.x, pt.y));
+                        }
                     }
+                    const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    finalCtrls = finalCtrls.concat(rightCtrls);
+                    newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                    return newink;
                 }
-                const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                finalCtrls = finalCtrls.concat(rightCtrls);
-                newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
-                return newink;
-            }
 
-            return ink.map((pt, i) => {
-                const leftHandlePoint = order === 0 && i === controlIndex + 1;
-                const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
-                if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) {
-                    return { X: pt.X + deltaX, Y: pt.Y + deltaY };
-                }
-                if (
-                    controlIndex === i ||
-                    leftHandlePoint ||
-                    rightHandlePoint ||
-                    (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
-                    ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
-                    (order === 3 && i === controlIndex - 1) ||
-                    (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
-                    (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
-                    (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))
-                ) {
-                    return { X: pt.X + deltaX, Y: pt.Y + deltaY };
-                }
-                return pt;
-            });
-        });
+                return ink.map((pt, i) => {
+                    const leftHandlePoint = order === 0 && i === controlIndex + 1;
+                    const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
+                    if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) {
+                        return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+                    }
+                    if (
+                        controlIndex === i ||
+                        leftHandlePoint ||
+                        rightHandlePoint ||
+                        (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+                        ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
+                        (order === 3 && i === controlIndex - 1) ||
+                        (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
+                        (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
+                        (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))
+                    ) {
+                        return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+                    }
+                    return pt;
+                });
+            }),
+        'move ink ctrl pt'
+    );
 
     public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
         let distance = Number.MAX_SAFE_INTEGER;
@@ -470,26 +471,28 @@ export class InkStrokeProperties {
     /**
      * Handles the movement/scaling of a handle point.
      */
-    @undoBatch
-    moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
-        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
-            const doc = view.Document;
-            const closed = InkingStroke.IsClosed(ink);
-            const oldHandlePoint = ink[handleIndex];
-            const oppositeHandlePoint = ink[oppositeHandleIndex];
-            const controlPoint = ink[controlIndex];
-            const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
-            const inkCopy = ink.slice();
-            inkCopy[handleIndex] = newHandlePoint;
-            const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
-            const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
-            // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
-            if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
-                const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
-                inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
-            }
-            return inkCopy;
-        });
+    moveTangentHandle = undoable(
+        (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+            this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+                const doc = view.Document;
+                const closed = InkingStroke.IsClosed(ink);
+                const oldHandlePoint = ink[handleIndex];
+                const oppositeHandlePoint = ink[oppositeHandleIndex];
+                const controlPoint = ink[controlIndex];
+                const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
+                const inkCopy = ink.slice();
+                inkCopy[handleIndex] = newHandlePoint;
+                const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
+                const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
+                // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
+                if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
+                    const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+                    inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+                }
+                return inkCopy;
+            }),
+        'move ink tangent'
+    );
 
     /**
      * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and
@@ -497,8 +500,7 @@ export class InkStrokeProperties {
      * @param inkDocs
      * @param tolerance Determines how strong the smooth effect will be
      */
-    @undoBatch
-    smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => {
+    smoothInkStrokes = undoable((inkDocs: Doc[], tolerance: number = 5) => {
         inkDocs.forEach(inkDoc => {
             const inkView = DocumentView.getDocumentView(inkDoc);
             const inkStroke = inkView?.ComponentView as InkingStroke;
@@ -530,5 +532,5 @@ export class InkStrokeProperties {
                 }
             }
         });
-    };
+    }, 'smooth ink stroke');
 }
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index b94041642..f346d4ba8 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -13,7 +13,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit
 import { RxWidth } from 'react-icons/rx';
 import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
 import { TfiBarChart } from 'react-icons/tfi';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
@@ -28,8 +28,6 @@ import { Colors } from './global/globalEnums';
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere } from './nodes/OpenWhere';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { MarqueeOptionsMenu } from './collections/collectionFreeForm';
-import { InkStrokeProperties } from './InkStrokeProperties';
 
 @observer
 export class PropertiesButtons extends React.Component {
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 98ddc61f9..486c826b6 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -847,7 +847,7 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch
                     styleProvider={this._props.styleProvider}
                     renderDepth={this._props.renderDepth + 1}
                     LayoutTemplate={undefined}
-                    LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
+                    LayoutTemplateString={LabelBox.LayoutString('data')}
                     isDocumentActive={this._props.isDocumentActive}
                     PanelWidth={width}
                     PanelHeight={height}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index a0e079158..a60cd98ac 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -50,7 +50,6 @@ export type collectionTreeViewProps = {
 export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() {
     public static AddTreeFunc = 'addTreeFolder(this.embedContainer)';
     private _treedropDisposer?: DragManager.DragDropDisposer;
-    private _mainEle?: HTMLDivElement;
     private _titleRef?: HTMLDivElement | HTMLInputElement | null;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _isDisposing = false; // notes that instance is in process of being disposed
@@ -81,8 +80,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
 
     @observable _titleHeight = 0; // height of the title bar
 
-    MainEle = () => this._mainEle;
-
     // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
     @observable _isAnyChildContentActive = false;
     whenChildContentsActiveChanged = action((isActive: boolean) => {
@@ -132,7 +129,6 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
     };
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer?.();
-        this._mainEle = ele;
         if (ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this));
     };
 
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 847ff5491..015f77ffd 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -472,7 +472,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref);
         return new Transform(-translateX, -translateY, 1).scale(1 / scale);
     };
-    docTransform = () => this.refTransform(this._dref?.ContentRef?.current);
+    docTransform = () => this.refTransform(this._dref?.ContentDiv);
     getTransform = () => this.refTransform(this._tref.current);
     embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1);
     embeddedPanelHeight = () => {
@@ -754,7 +754,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
     }
 
     get onCheckedClick() {
-        return this.Document.type === DocumentType.COL ? undefined : this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick);
+        return this.Document.type === DocumentType.COL ? undefined : (this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick));
     }
 
     @action
@@ -779,7 +779,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
         TraceMobx();
         const iconType = (this.treeView._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':treeOpen' : !this.childDocs.length ? ':empty' : '')) as string) || 'question';
         const color = SettingsManager.userColor;
-        const checked = this.onCheckedClick ? this.Document.treeView_Checked ?? 'unchecked' : undefined;
+        const checked = this.onCheckedClick ? (this.Document.treeView_Checked ?? 'unchecked') : undefined;
         return (
             <div
                 className={`bullet${this.treeView.outlineMode ? '-outline' : ''}`}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d95bc528d..d51b1cd3a 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -328,6 +328,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 }
 // eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) {
+ScriptingGlobals.add(function gotoFrame(doc: Doc, newFrame: number) {
     CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame);
 });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3ff3f49dc..4cc7eb0ff 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -156,13 +156,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     /// disable pointer events on content when there's an enabled onClick script (and not in explore mode) and the contents aren't forced active, or if contents are marked inactive
     @computed get _contentPointerEvents() {
         TraceMobx();
-        return this._props.contentPointerEvents ??
+        return (this._props.contentPointerEvents ??
             ((!this.disableClickScriptFunc && //
                 this.onClickHdlr &&
                 !SnappingManager.ExploreMode &&
                 !this.layoutDoc.layout_isSvg &&
                 this.isContentActive() !== true) ||
-                this.isContentActive() === false)
+                this.isContentActive() === false))
             ? 'none'
             : this._pointerEvents;
     }
@@ -892,7 +892,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         const showTitle = this.showTitle?.split(':')[0];
         return !DocCast(this.Document) || GetEffectiveAcl(this.dataDoc) === AclPrivate
             ? null
-            : this.docContents ?? (
+            : (this.docContents ?? (
                   <div
                       className="documentView-node"
                       id={this.Document.type !== DocumentType.LINK ? this._docView?.DocUniqueId : undefined}
@@ -918,7 +918,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                       )}
                       {this.widgetDecorations ?? null}
                   </div>
-              );
+              ));
     };
 
     render() {
@@ -994,7 +994,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             bottom: dir === PresEffectDirection.Bottom,
             opposite: true,
             delay: 0,
-            duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
+            duration,
         };
 
         const timing = StrCast(presEffectDoc?.presentation_effectTiming);
@@ -1083,13 +1083,16 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
     ) => Promise<void>;
     public static linkCommonAncestor: (link: Doc) => DocumentView | undefined;
-    // pin func
+    /**
+     * Pins a Doc to the current presentation trail. (see TabDocView for implementation)
+     */
     public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void;
-    // gesture
+    /**
+     * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
+     */
     public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
 
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
-    public ContentRef = React.createRef<HTMLDivElement>();
     private _htmlOverlayEffect: Opt<Doc>;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _viewTimer: NodeJS.Timeout | undefined;
@@ -1468,11 +1471,10 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                 {!this.Document || !this._props.PanelWidth() ? null : (
                     <div
                         className="contentFittingDocumentView-previewDoc"
-                        ref={this.ContentRef}
                         style={{
                             transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
                             width: xshift ?? `${this._props.PanelWidth() - this.Xshift * 2}px`,
-                            height: this._props.forceAutoHeight ? undefined : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`),
+                            height: this._props.forceAutoHeight ? undefined : (yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(this.effectiveNativeHeight / this.effectiveNativeWidth) * this._props.PanelWidth()}px`)),
                         }}>
                         <DocumentViewInternal
                             {...this._props}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0c0667575..38b22fad2 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -101,9 +101,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection
     static _globalHighlightsCache: string = '';
     static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
-    static _highlightStyleSheet: any = addStyleSheet();
-    static _bulletStyleSheet: any = addStyleSheet();
-    static _userStyleSheet: any = addStyleSheet();
+    static _highlightStyleSheet = addStyleSheet();
+    static _bulletStyleSheet = addStyleSheet();
+    static _userStyleSheet = addStyleSheet();
     static _hadSelection: boolean = false;
     private _oldWheel: HTMLDivElement | null = null;
     private _selectionHTML: string | undefined;
@@ -361,7 +361,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     }
                 }
             } else {
-                const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data!;
+                const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data;
                 if (jsonstring) {
                     const json = JSON.parse(jsonstring);
                     json.selection = state.toJSON().selection;
@@ -1897,11 +1897,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                     setHeight={this.setSidebarHeight}
                 />
             ) : (
-                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}>
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
                     <ComponentTag
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
-                        ref={this._sidebarTagRef as any}
+                        ref={this._sidebarTagRef}
                         setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 7dd4047c1..3bd42873c 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -111,6 +111,7 @@ export class Annotation extends ObservableReactComponent<IAnnotationProps> {
     outline = () => (this.linkHighlighted ? 'solid 1px lightBlue' : undefined);
     background = () => (this._props.annoDoc[Highlight] ? 'orange' : StrCast(this._props.annoDoc.backgroundColor));
     render() {
+        const forceRenderHack = [this.background(), this.outline(), this.opacity()]; // forces a re-render when these change -- because RegionAnnotation doesn't do this internally..
         return (
             <div style={{ display: this._props.annoDoc.textCopied && !Doc.GetBrushHighlightStatus(this._props.annoDoc) ? 'none' : undefined }}>
                 {StrListCast(this._props.annoDoc.text_inlineAnnotations)
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5984905d0..20719442a 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -52,7 +52,7 @@ interface IViewerProps extends FieldViewProps {
  */
 @observer
 export class PDFViewer extends ObservableReactComponent<IViewerProps> {
-    static _annotationStyle: any = addStyleSheet();
+    static _annotationStyle = addStyleSheet();
 
     constructor(props: IViewerProps) {
         super(props);
@@ -522,7 +522,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         if (doc instanceof Doc && property === StyleProp.PointerEvents) {
             if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none';
             const isInk = doc.layout_isSvg && !props?.LayoutTemplateString;
-            return isInk ? 'visiblePainted' : 'all';
+            if (isInk) return 'visiblePainted';
         }
         return this._props.styleProvider?.(doc, props, property);
     };
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index c296138a8..22b8b7b67 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -6,25 +6,25 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
+import { Copy } from '../../../fields/FieldSymbols';
 import { ImageCast } from '../../../fields/Types';
-import { emptyFunction } from '../../../Utils';
+import { ImageField } from '../../../fields/URLField';
+import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { makeUserTemplateImage } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
 import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { DefaultStyleProvider } from '../StyleProvider';
 import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { ImageField } from '../../../fields/URLField';
-import { Copy } from '../../../fields/FieldSymbols';
 
 interface AnnotationPaletteProps {
     Document: Doc;
@@ -204,7 +204,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                             addDocument={undefined}
                             addDocTab={DocumentViewInternal.addDocTabFunc}
                             pinToPres={DocumentView.PinDoc}
-                            containerViewPath={returnEmptyDoclist}
+                            containerViewPath={returnEmptyDocViewList}
                             styleProvider={DefaultStyleProvider}
                             removeDocument={returnFalse}
                             ScreenToLocalTransform={Transform.Identity}
@@ -328,7 +328,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                             addDocument={undefined}
                             addDocTab={DocumentViewInternal.addDocTabFunc}
                             pinToPres={DocumentView.PinDoc}
-                            containerViewPath={returnEmptyDoclist}
+                            containerViewPath={returnEmptyDocViewList}
                             styleProvider={DefaultStyleProvider}
                             removeDocument={returnFalse}
                             ScreenToLocalTransform={Transform.Identity}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 368970f75..4679941fb 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -279,7 +279,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            svgStrokes.forEach((child: any) => {
+            svgStrokes.forEach((child) => {
                 const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
                 strokeData.push([
                     convertedBezier.map(point => {
-- 
cgit v1.2.3-70-g09d2


From b1692a7435ae9698eff618bef0e370fe3eb89572 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Aug 2024 13:59:00 -0400
Subject: from last cleanup

---
 src/client/views/InkStrokeProperties.ts            | 140 +++++------
 src/client/views/LightboxView.tsx                  |   3 +-
 src/client/views/MainView.tsx                      |   4 +-
 src/client/views/MarqueeAnnotator.tsx              |   9 +-
 src/client/views/PropertiesView.tsx                |  70 ++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  24 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  10 +-
 src/client/views/global/globalScripts.ts           |   4 -
 src/client/views/nodes/LabelBigText.js             | 270 ---------------------
 .../views/nodes/formattedText/FormattedTextBox.tsx |   6 +-
 src/client/views/pdf/AnchorMenu.tsx                |  15 +-
 src/client/views/pdf/Annotation.tsx                |   3 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  10 +-
 13 files changed, 127 insertions(+), 441 deletions(-)
 delete mode 100644 src/client/views/nodes/LabelBigText.js

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

diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index b5cd72fa7..13807c25f 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,22 +1,22 @@
 import { Bezier } from 'bezier-js';
 import * as _ from 'lodash';
 import { action, makeObservable, observable, reaction, runInAction } from 'mobx';
+import simplify from 'simplify-js';
 import { Doc, NumListCast, Opt } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { listSpec } from '../../fields/Schema';
-import { Cast, NumCast, toList } from '../../fields/Types';
+import { Cast, NumCast } from '../../fields/Types';
 import { Gestures, PointData } from '../../pen-gestures/GestureTypes';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
 import { Point } from '../../pen-gestures/ndollar';
 import { DocumentType } from '../documents/DocumentTypes';
-import { undoBatch, undoable } from '../util/UndoManager';
+import { undoable } from '../util/UndoManager';
 import { FitOneCurve } from '../util/bezierFit';
+import { GestureOverlay } from './GestureOverlay';
 import { InkingStroke } from './InkingStroke';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
-import simplify from 'simplify-js';
-import { GestureUtils } from '../../pen-gestures/GestureUtils';
-import { GestureOverlay } from './GestureOverlay';
 
 export class InkStrokeProperties {
     // eslint-disable-next-line no-use-before-define
@@ -163,48 +163,46 @@ export class InkStrokeProperties {
     /**
      * Deletes the current control point of the selected ink instance.
      */
-    deletePoints = undoable(
-        (inkView: DocumentView, preserve: boolean) =>
-            this.applyFunction(
-                inkView,
-                (view: DocumentView, ink: InkData) => {
-                    const doc = view.Document;
-                    const newPoints = ink.slice();
-                    const brokenIndices = NumListCast(doc.brokenInkIndices);
-                    if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
-                        newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
-                    } else {
-                        const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
-                        const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
-                        const samples: Point[] = [];
-                        let startDir = { x: 0, y: 0 };
-                        let endDir = { x: 0, y: 0 };
-                        for (let i = 0; i < splicedPoints.length / 4; i++) {
-                            const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
-                            if (i === 0) startDir = bez.derivative(0);
-                            if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
-                            for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
-                                const pt = bez.compute(t);
-                                samples.push(new Point(pt.x, pt.y));
-                            }
-                        }
-                        const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
-                        if (error < 100) {
-                            newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
-                        } else {
-                            newPoints.splice(this._currentPoint - 2, 4);
+    deletePoints = undoable((inkView: DocumentView, preserve: boolean) => {
+        this.applyFunction(
+            inkView,
+            (view: DocumentView, ink: InkData) => {
+                const doc = view.Document;
+                const newPoints = ink.slice();
+                const brokenIndices = NumListCast(doc.brokenInkIndices);
+                if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
+                    newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
+                } else {
+                    const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+                    const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+                    const samples: Point[] = [];
+                    let startDir = { x: 0, y: 0 };
+                    let endDir = { x: 0, y: 0 };
+                    for (let i = 0; i < splicedPoints.length / 4; i++) {
+                        const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+                        if (i === 0) startDir = bez.derivative(0);
+                        if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+                        for (let t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+                            const pt = bez.compute(t);
+                            samples.push(new Point(pt.x, pt.y));
                         }
                     }
-                    doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
-                    runInAction(() => {
-                        this._currentPoint = -1;
-                    });
-                    return newPoints.length < 4 ? undefined : newPoints;
-                },
-                true
-            ),
-        'delete ink points'
-    );
+                    const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+                    if (error < 100) {
+                        newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+                    } else {
+                        newPoints.splice(this._currentPoint - 2, 4);
+                    }
+                }
+                doc.brokenInkIndices = new List(brokenIndices.map(control => (control >= this._currentPoint ? control - 4 : control)));
+                runInAction(() => {
+                    this._currentPoint = -1;
+                });
+                return newPoints.length < 4 ? undefined : newPoints;
+            },
+            true
+        );
+    }, 'delete ink points');
 
     /**
      * Rotates ink stroke(s) about a point
@@ -251,9 +249,8 @@ export class InkStrokeProperties {
     /**
      * Handles the movement/scaling of a control point.
      */
-    moveControlPtHandle = undoable(
-        (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
-            inkView &&
+    moveControlPtHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) => {
+        inkView &&
             this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
                 const order = controlIndex % 4;
                 const closed = InkingStroke.IsClosed(ink);
@@ -316,9 +313,8 @@ export class InkStrokeProperties {
                     }
                     return pt;
                 });
-            }),
-        'move ink ctrl pt'
-    );
+            });
+    }, 'move ink ctrl pt');
 
     public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
         let distance = Number.MAX_SAFE_INTEGER;
@@ -471,28 +467,26 @@ export class InkStrokeProperties {
     /**
      * Handles the movement/scaling of a handle point.
      */
-    moveTangentHandle = undoable(
-        (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
-            this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
-                const doc = view.Document;
-                const closed = InkingStroke.IsClosed(ink);
-                const oldHandlePoint = ink[handleIndex];
-                const oppositeHandlePoint = ink[oppositeHandleIndex];
-                const controlPoint = ink[controlIndex];
-                const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
-                const inkCopy = ink.slice();
-                inkCopy[handleIndex] = newHandlePoint;
-                const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
-                const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
-                // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
-                if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
-                    const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
-                    inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
-                }
-                return inkCopy;
-            }),
-        'move ink tangent'
-    );
+    moveTangentHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => {
+        this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+            const doc = view.Document;
+            const closed = InkingStroke.IsClosed(ink);
+            const oldHandlePoint = ink[handleIndex];
+            const oppositeHandlePoint = ink[oppositeHandleIndex];
+            const controlPoint = ink[controlIndex];
+            const newHandlePoint = { X: ink[handleIndex].X - deltaX, Y: ink[handleIndex].Y - deltaY };
+            const inkCopy = ink.slice();
+            inkCopy[handleIndex] = newHandlePoint;
+            const brokenIndices = Cast(doc.brokenInkIndices, listSpec('number'));
+            const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1;
+            // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
+            if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
+                const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+                inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+            }
+            return inkCopy;
+        });
+    }, 'move ink tangent');
 
     /**
      * Function that "smooths" ink strokes by using the gesture recognizer to detect shapes and
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 651fb5888..77a2c1b86 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -22,7 +22,6 @@ import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './
 import { DocumentView } from './nodes/DocumentView';
 import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
 import { AnnotationPalette } from './smartdraw/AnnotationPalette';
-import { DocData } from '../../fields/DocSymbols';
 
 interface LightboxViewProps {
     PanelWidth: number;
@@ -58,6 +57,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     }[] = [];
     private _savedState: LightboxSavedState = {};
     private _history: { doc: Doc; target?: Doc }[] = [];
+    private _annoPaletteView: AnnotationPalette | null = null;
     @observable private _future: Doc[] = [];
     @observable private _layoutTemplate: Opt<Doc> = undefined;
     @observable private _layoutTemplateString: Opt<string> = undefined;
@@ -65,7 +65,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
     @observable private _docTarget: Opt<Doc> = undefined;
     @observable private _docView: Opt<DocumentView> = undefined;
     @observable private _showPalette: boolean = false;
-    private _annoPaletteView: AnnotationPalette | null = null;
 
     @computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore
     @computed get topBorder()  { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2a983a29f..6118cee92 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -580,9 +580,7 @@ export class MainView extends ObservableReactComponent<object> {
         DocumentManager.removeOverlayViews();
         Doc.linkFollowUnhighlight();
         const targets = document.elementsFromPoint(e.x, e.y);
-        const targetClasses: string[] = targets.map(target => {
-            return target.className.toString();
-        });
+        const targetClasses = targets.map(target => target.className.toString());
         if (targets.length) {
             let targClass = targets[0].className.toString();
             for (let i = 0; i < targets.length - 1; i++) {
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 0d2a11ec4..90323086c 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -27,7 +27,7 @@ export interface MarqueeAnnotatorProps {
     containerOffset?: () => number[];
     marqueeContainer: HTMLDivElement;
     docView: () => DocumentView;
-    savedAnnotations: () => ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>;
+    savedAnnotations: () => ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>;
     selectionText: () => string;
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
@@ -60,7 +60,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
     });
 
     @undoBatch
-    makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>): Opt<Doc> => {
+    makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>): Opt<Doc> => {
         const savedAnnoMap = savedAnnotations?.values() && Array.from(savedAnnotations?.values()).length ? savedAnnotations : this.props.savedAnnotations();
         if (savedAnnoMap.size === 0) return undefined;
         const savedAnnos = Array.from(savedAnnoMap.values())[0];
@@ -128,7 +128,6 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         savedAnnoMap.clear();
         return textRegionAnno;
     };
-
     @action
     highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
         // creates annotation documents for current highlights
@@ -138,7 +137,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         return annotationDoc as Doc;
     };
 
-    public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>, annotationLayer: HTMLDivElement & { marqueeing?: boolean}, div: HTMLDivElement, page: number) => {
+    public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>, annotationLayer: HTMLDivElement & { marqueeing?: boolean }, div: HTMLDivElement, page: number) => {
         div.style.backgroundColor = '#ACCEF7';
         div.style.opacity = '0.5';
         annotationLayer.append(div);
@@ -266,7 +265,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
         if (!this.isEmpty && marqueeStyle) {
             // configure and show the annotation/link menu if a the drag region is big enough
             // copy the temporary marquee to allow for multiple selections (not currently available though).
-            const copy: (HTMLDivElement & {marqueeing?: boolean}) = document.createElement('div');
+            const copy: HTMLDivElement & { marqueeing?: boolean } = document.createElement('div');
             const scale = (this.props.scaling?.() || 1) * NumCast(this.props.Document._freeform_scale, 1);
             ['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => {
                 copy.style[prop as unknown as number] = marqueeStyle[prop as unknown as number]; // bcz: hack to get around TS type checking for array index with strings
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index bbe422e9e..6383d4947 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -813,6 +813,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
         return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
     }
 
+    @computed get selectedStrokes() {
+        return this.containsInkDoc ? DocListCast(this.selectedDoc[DocData].data) : this.selectedDoc ? [this.selectedDoc] : [];
+    }
     @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
     set shapeXps(value)  { this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } // prettier-ignore
     @computed get shapeYps() { return NumCast(this.selectedDoc?.y); } // prettier-ignore
@@ -822,16 +825,11 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @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 this.containsInkDoc ?  NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
-    set strokeThk(value) { 
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_width = Math.round(value * 100) / 100;
-            })
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); 
-        }
-    } // prettier-ignore
+    set strokeThk(value) {
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_width = Math.round(value * 100) / 100;
+        });
+    }
 
     @computed get hgtInput() {
         return this.inputBoxDuo(
@@ -885,14 +883,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
     set colorStk(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].color = value || undefined;
-            });
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined);
-        }
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].color = value || undefined;
+        });
     }
 
     colorButton(value: string, type: string, setter: () => void) {
@@ -1019,14 +1012,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     @computed get dashdStk() {  return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore
     set dashdStk(value) {
         value && (this._lastDash = value as string);
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_dash = value ? this._lastDash : undefined;
-            });
-        } else {
-            this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
-        }
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_dash = value ? this._lastDash : undefined;
+        });
     }
     @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
     set widthStk(value) {
@@ -1034,13 +1022,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get markScal() { return Number(this.getField('stroke_markerScale') || '1'); } // prettier-ignore
     set markScal(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_markerScale = Number(value);
-            });
-        }
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_markerScale = Number(value);
+        });
     }
     @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '10'); } // prettier-ignore
     set smoothAmt(value) {
@@ -1048,23 +1032,15 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
     }
     @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
     set markHead(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_startMarker = value;
-            });
-        }
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_startMarker = value;
+        });
     }
     @computed get markTail() {  return this.getField('stroke_endMarker') || ''; } // prettier-ignore
     set markTail(value) {
-        if (this.containsInkDoc) {
-            const childDocs = DocListCast(this.selectedDoc[DocData].data);
-            childDocs.forEach(doc => {
-                doc[DocData].stroke_endMarker = value;
-            });
-        }
-        this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+        this.selectedStrokes.forEach(doc => {
+            doc[DocData].stroke_endMarker = value;
+        });
     }
 
     regInput = (key: string, value: string | number | undefined, setter: (val: string) => void) => (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2963a8f53..7c7764f1d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -89,8 +89,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     private _clusters = new CollectionFreeFormClusters(this);
-    private _oldWheel: any;
-    private _panZoomTransitionTimer: any;
+    private _oldWheel: HTMLDivElement | null = null;
+    private _panZoomTransitionTimer: NodeJS.Timeout | undefined = undefined;
+    private _brushtimer: NodeJS.Timeout | undefined = undefined;
+    private _brushtimer1: NodeJS.Timeout | undefined = undefined;
     private _lastX: number = 0;
     private _lastY: number = 0;
     private _downX: number = 0;
@@ -99,8 +101,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _renderCutoffData = observable.map<string, boolean>();
     private _batch: UndoManager.Batch | undefined = undefined;
-    private _brushtimer: any;
-    private _brushtimer1: any;
     private _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
 
     private _presEaseFunc: string = 'ease';
@@ -142,12 +142,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @computed get childPointerEvents() {
         return SnappingManager.IsResizing
             ? 'none'
-            : this._props.childPointerEvents?.() ??
+            : (this._props.childPointerEvents?.() ??
                   (this._props.viewDefDivClick || //
                   (this.layoutEngine === computePassLayout.name && !this._props.isSelected()) ||
                   this.isContentActive() === false
                       ? 'none'
-                      : this._props.pointerEvents?.());
+                      : this._props.pointerEvents?.()));
     }
     @computed get contentViews() {
         const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
@@ -376,14 +376,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const xfToCollection = options?.docTransform ?? Transform.Identity();
         const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
         const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc());
-        const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale ?? 0.75 : undefined);
+        const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined);
 
         // focus on the document in the collection
         const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
         if (didMove) options.didMove = true;
         // glr: freeform transform speed can be set by adjusting presentation_transition field - needs a way of knowing when presentation is not active...
         if (didMove) {
-            const focusTime = options?.instant ? 0 : options.zoomTime ?? 500;
+            const focusTime = options?.instant ? 0 : (options.zoomTime ?? 500);
             (options.zoomScale ?? options.willZoomCentered) && scale && (this.Document[this.scaleFieldKey] = scale);
             this.setPan(panX, panY, focusTime); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             return focusTime;
@@ -1546,7 +1546,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return ret;
     };
     childPointerEventsFunc = () => this._childPointerEvents;
-    childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
+    childContentsActive = () => ((this._props.childContentsActive ?? this.isContentActive() === false) ? returnFalse : emptyFunction)();
     getChildDocView(entry: PoolData) {
         const childLayout = entry.pair.layout;
         const childData = entry.pair.data;
@@ -1722,7 +1722,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * since rendering a large collection of documents can be slow, at startup, docs are rendered in batches.
      * each doc's render() method will call the cutoff provider which will let the doc know if it should render itself yet, or wait
      */
-    renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc ? false : !this._renderCutoffData.get(doc[Id] + '')));
+    renderCutoffProvider = computedFn((doc: Doc) => (this.Document.isTemplateDoc || this.Document.isTemplateForField ? false : !this._renderCutoffData.get(doc[Id] + '')));
 
     doEngineLayout(
         poolData: Map<string, PoolData>,
@@ -1982,7 +1982,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 icon: 'eye',
             });
         appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' });
-        !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
+        !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
         this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
         this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
@@ -2080,7 +2080,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     incrementalRender = action(() => {
         if (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())) {
             const layoutUnrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
-            const loadIncrement = this.Document.isTemplateDoc ? Number.MAX_VALUE : 5;
+            const loadIncrement = this.Document.isTemplateDoc || this.Document.isTemplateForField ? Number.MAX_VALUE : 5;
             for (let i = 0; i < Math.min(layoutUnrendered.length, loadIncrement); i++) {
                 this._renderCutoffData.set(layoutUnrendered[i][Id] + '', true);
             }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f85727661..bfb1d12cb 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -8,7 +8,7 @@ import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSy
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
-import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
 import { ImageField } from '../../../../fields/URLField';
 import { GetEffectiveAcl } from '../../../../fields/util';
 import { DocUtils } from '../../../documents/DocUtils';
@@ -22,7 +22,7 @@ import { MainView } from '../../MainView';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { MarqueeViewBounds } from '../../PinFuncs';
 import { PreviewCursor } from '../../PreviewCursor';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../../nodes/DocumentView';
+import { DocumentView } from '../../nodes/DocumentView';
 import { OpenWhere } from '../../nodes/OpenWhere';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -30,12 +30,6 @@ import { SubCollectionViewProps } from '../CollectionSubView';
 import { ImageLabelBoxData } from './ImageLabelBox';
 import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
 import './MarqueeView.scss';
-import { collectionOf, points } from '@turf/turf';
-import { InkingStroke } from '../../InkingStroke';
-import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { Gestures } from '../../../../pen-gestures/GestureTypes';
-import { GestureOverlay } from '../../GestureOverlay';
-import { InkStrokeProperties } from '../../InkStrokeProperties';
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 50319c466..68d287ac8 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -36,9 +36,6 @@ import { ImageBox } from '../nodes/ImageBox';
 import { VideoBox } from '../nodes/VideoBox';
 import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-// import { InkTranscription } from '../InkTranscription';
-
-// import { InkTranscription } from '../InkTranscription';
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
@@ -367,7 +364,6 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
 export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) {
     // TODO nda - if document being added to is a inkGrouping then we can just add to that group
     if (Doc.ActiveTool === InkTool.Write) {
-        console.log('create inking group ');
         CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
             // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
             const selected = ffView.unprocessedDocs;
diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js
deleted file mode 100644
index 290152cd0..000000000
--- a/src/client/views/nodes/LabelBigText.js
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
-Brorlandi/big-text.js v1.0.0, 2017
-Adapted from DanielHoffmann/jquery-bigtext, v1.3.0, May 2014
-And from Jetroid/bigtext.js v1.0.0, September 2016
-
-Usage:
-BigText("#myElement",{
-    rotateText: {Number}, (null)
-    fontSizeFactor: {Number}, (0.8)
-    maximumFontSize: {Number}, (null)
-    limitingDimension: {String}, ("both")
-    horizontalAlign: {String}, ("center")
-    verticalAlign: {String}, ("center")
-    textAlign: {String}, ("center")
-    whiteSpace: {String}, ("nowrap")
-});
-
-
-Original Projects: 
-https://github.com/DanielHoffmann/jquery-bigtext
-https://github.com/Jetroid/bigtext.js
-
-Options:
-
-rotateText: Rotates the text inside the element by X degrees.
-
-fontSizeFactor: This option is used to give some vertical spacing for letters that overflow the line-height (like 'g', 'Á' and most other accentuated uppercase letters). This does not affect the font-size if the limiting factor is the width of the parent div. The default is 0.8
-
-maximumFontSize: maximum font size to use.
-
-minimumFontSize: minimum font size to use.  if font is calculated smaller than this, text will be rendered at this size and wrapped
-
-limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height.
-
-horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center".
-
-verticalAlign: Where to align the text vertically. Possible values: "top", "center", "bottom". Defaults to "center".
-
-textAlign: Sets the text align of the element. Possible values: "left", "center", "right". Defaults to "center". This option is only useful if there are linebreaks (<br> tags) inside the text.
-
-whiteSpace: Sets whitespace handling. Possible values: "nowrap", "pre". Defaults to "nowrap". (Can also be set to enable wrapping but this doesn't work well.)
-
-Bruno Orlandi - 2017
-
-Copyright (C) 2013 Daniel Hoffmann Bernardes, Ícaro Technologies
-Copyright (C) 2016 Jet Holt
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-function _calculateInnerDimensions(computedStyle) {
-    //Calculate the inner width and height
-    var innerWidth;
-    var innerHeight;
-
-    var width = parseInt(computedStyle.getPropertyValue("width"));
-    var height = parseInt(computedStyle.getPropertyValue("height"));
-    var paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left"));
-    var paddingRight = parseInt(computedStyle.getPropertyValue("padding-right"));
-    var paddingTop = parseInt(computedStyle.getPropertyValue("padding-top"));
-    var paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom"));
-    var borderLeft = parseInt(computedStyle.getPropertyValue("border-left-width"));
-    var borderRight = parseInt(computedStyle.getPropertyValue("border-right-width"));
-    var borderTop = parseInt(computedStyle.getPropertyValue("border-top-width"));
-    var borderBottom = parseInt(computedStyle.getPropertyValue("border-bottom-width"));
-
-    //If box-sizing is border-box, we need to subtract padding and border.
-    var parentBoxSizing = computedStyle.getPropertyValue("box-sizing");
-    if (parentBoxSizing == "border-box") {
-        innerWidth = width - (paddingLeft + paddingRight + borderLeft + borderRight);
-        innerHeight = height - (paddingTop + paddingBottom + borderTop + borderBottom);
-    } else {
-        innerWidth = width;
-        innerHeight = height;
-    }
-    var obj = {};
-    obj["width"] = innerWidth;
-    obj["height"] = innerHeight;
-    return obj;
-}
-
-export default function BigText(element, options) {
-
-    if (typeof element === 'string') {
-        element = document.querySelector(element);
-    } else if (element.length) {
-        // Support for array based queries (such as jQuery)
-        element = element[0];
-    }
-
-    var defaultOptions = {
-        rotateText: null,
-        fontSizeFactor: 0.8,
-        maximumFontSize: null,
-        limitingDimension: "both",
-        horizontalAlign: "center",
-        verticalAlign: "center",
-        textAlign: "center",
-        whiteSpace: "nowrap",
-        singleLine: true
-    };
-
-    //Merge provided options and default options
-    options = options || {};
-    for (var opt in defaultOptions)
-        if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt))
-            options[opt] = defaultOptions[opt];
-
-    //Get variables which we will reference frequently
-    var style = element.style;
-    var parent = element.parentNode;
-    var parentStyle = parent.style;
-    var parentComputedStyle = document.defaultView.getComputedStyle(parent);
-
-    //hides the element to prevent "flashing"
-    style.visibility = "hidden";
-    //Set some properties
-    style.display = "inline-block";
-    style.clear = "both";
-    style.float = "left";
-    var fontSize = options.maximumFontSize;
-    if (options.singleLine) {
-        style.fontSize = (fontSize * options.fontSizeFactor) + "px";
-        style.lineHeight = fontSize + "px";
-    } else {
-        for (; fontSize > options.minimumFontSize; fontSize = fontSize - Math.min(fontSize / 2, Math.max(0, fontSize - 48) + 2)) {
-            style.fontSize = (fontSize * options.fontSizeFactor) + "px";
-            style.lineHeight = "1";
-            if (element.offsetHeight <= +parentComputedStyle.height.replace("px", "")) {
-                break;
-            }
-        }
-    }
-    style.whiteSpace = options.whiteSpace;
-    style.textAlign = options.textAlign;
-    style.position = "relative";
-    style.padding = 0;
-    style.margin = 0;
-    style.left = "50%";
-    style.top = "50%";
-    var computedStyle = document.defaultView.getComputedStyle(element);
-
-    //Get properties of parent to allow easier referencing later.
-    var parentPadding = {
-        top: parseInt(parentComputedStyle.getPropertyValue("padding-top")),
-        right: parseInt(parentComputedStyle.getPropertyValue("padding-right")),
-        bottom: parseInt(parentComputedStyle.getPropertyValue("padding-bottom")),
-        left: parseInt(parentComputedStyle.getPropertyValue("padding-left")),
-    };
-    var parentBorder = {
-        top: parseInt(parentComputedStyle.getPropertyValue("border-top")),
-        right: parseInt(parentComputedStyle.getPropertyValue("border-right")),
-        bottom: parseInt(parentComputedStyle.getPropertyValue("border-bottom")),
-        left: parseInt(parentComputedStyle.getPropertyValue("border-left")),
-    };
-
-    //Calculate the parent inner width and height
-    var parentInnerDimensions = _calculateInnerDimensions(parentComputedStyle);
-    var parentInnerWidth = parentInnerDimensions["width"];
-    var parentInnerHeight = parentInnerDimensions["height"];
-
-    var box = {
-        width: element.offsetWidth, //Note: This is slightly larger than the jQuery version
-        height: element.offsetHeight,
-    };
-    if (!box.width || !box.height) return element;
-
-
-    if (options.rotateText !== null) {
-        if (typeof options.rotateText !== "number")
-            throw "bigText error: rotateText value must be a number";
-        var rotate = "rotate(" + options.rotateText + "deg)";
-        style.webkitTransform = rotate;
-        style.msTransform = rotate;
-        style.MozTransform = rotate;
-        style.OTransform = rotate;
-        style.transform = rotate;
-        //calculating bounding box of the rotated element
-        var sine = Math.abs(Math.sin(options.rotateText * Math.PI / 180));
-        var cosine = Math.abs(Math.cos(options.rotateText * Math.PI / 180));
-        box.width = element.offsetWidth * cosine + element.offsetHeight * sine;
-        box.height = element.offsetWidth * sine + element.offsetHeight * cosine;
-    }
-
-    var parentWidth = (parentInnerWidth - parentPadding.left - parentPadding.right);
-    var parentHeight = (parentInnerHeight - parentPadding.top - parentPadding.bottom);
-    var widthFactor = parentWidth / box.width;
-    var heightFactor = parentHeight / box.height;
-    var lineHeight;
-
-    if (options.limitingDimension.toLowerCase() === "width") {
-        lineHeight = Math.floor(widthFactor * fontSize);
-    } else if (options.limitingDimension.toLowerCase() === "height") {
-        lineHeight = Math.floor(heightFactor * fontSize);
-    } else if (widthFactor < heightFactor)
-        lineHeight = Math.floor(widthFactor * fontSize);
-    else if (widthFactor >= heightFactor)
-        lineHeight = Math.floor(heightFactor * fontSize);
-
-    var fontSize = lineHeight * options.fontSizeFactor;
-    if (fontSize < options.minimumFontSize) {
-        parentStyle.display = "flex";
-        parentStyle.alignItems = "center";
-        style.textAlign = "center";
-        style.visibility = "";
-        style.fontSize = options.minimumFontSize + "px";
-        style.lineHeight = "";
-        style.overflow = "hidden";
-        style.textOverflow = "ellipsis";
-        style.top = "";
-        style.left = "";
-        style.margin = "";
-        return element;
-    }
-    if (options.maximumFontSize && fontSize > options.maximumFontSize) {
-        fontSize = options.maximumFontSize;
-        lineHeight = fontSize / options.fontSizeFactor;
-    }
-
-    style.fontSize = Math.floor(fontSize) + "px";
-    style.lineHeight = Math.ceil(lineHeight) + "px";
-    style.marginBottom = "0px";
-    style.marginRight = "0px";
-
-    // if (options.limitingDimension.toLowerCase() === "height") {
-    //     //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size
-    //     //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code
-    //     parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px";
-    // }
-
-    //Calculate the inner width and height
-    var innerDimensions = _calculateInnerDimensions(computedStyle);
-    var innerWidth = innerDimensions["width"];
-    var innerHeight = innerDimensions["height"];
-
-    switch (options.verticalAlign.toLowerCase()) {
-        case "top":
-            style.top = "0%";
-            break;
-        case "bottom":
-            style.top = "100%";
-            style.marginTop = Math.floor(-innerHeight) + "px";
-            break;
-        default:
-            style.marginTop = Math.ceil((-innerHeight / 2)) + "px";
-            break;
-    }
-
-    switch (options.horizontalAlign.toLowerCase()) {
-        case "left":
-            style.left = "0%";
-            break;
-        case "right":
-            style.left = "100%";
-            style.marginLeft = Math.floor(-innerWidth) + "px";
-            break;
-        default:
-            style.marginLeft = Math.ceil((-innerWidth / 2)) + "px";
-            break;
-    }
-
-    //shows the element after the work is done
-    style.visibility = "visible";
-
-    return element;
-}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 38b22fad2..996c6843e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -265,11 +265,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         });
 
         AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
-            const container = DocCast(this._props.Document.embedContainer);
+            const container = DocCast(this.Document.embedContainer);
             const docView = DocumentView.getDocumentView?.(container);
             docView?.ComponentView?._props.addDocument?.(drawing);
-            drawing.x = NumCast(this._props.Document.x) + (this._props.Document.width as number);
-            drawing.y = NumCast(this._props.Document.y);
+            drawing.x = NumCast(this.Document.x) + (this.Document.width as number);
+            drawing.y = NumCast(this.Document.y);
         };
 
         AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 4e0c73b02..1a79bbbfe 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,25 +1,24 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ColorResult } from 'react-color';
+import ReactLoading from 'react-loading';
 import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
 import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { SettingsManager } from '../../util/SettingsManager';
+import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
 import { LinkPopup } from '../linking/LinkPopup';
 import { DocumentView } from '../nodes/DocumentView';
+import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import './AnchorMenu.scss';
 import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
-import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
-import { InkData, InkField } from '../../../fields/InkField';
-import { DocData } from '../../../fields/DocSymbols';
-import { undoBatch } from '../../util/UndoManager';
-import ReactLoading from 'react-loading';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -149,9 +148,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     gptDraw = async (e: React.PointerEvent) => {
         try {
             SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
-            this._isLoading = true;
+            runInAction(() => (this._isLoading = true));
             await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
-            this._isLoading = false;
+            runInAction(() => (this._isLoading = false));
         } catch (err) {
             console.error(err);
         }
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 3bd42873c..1891cfd4c 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -13,6 +13,7 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { OpenWhere } from '../nodes/OpenWhere';
 import { AnchorMenu } from './AnchorMenu';
 import './Annotation.scss';
+import { Property } from 'csstype';
 
 interface IRegionAnnotationProps {
     x: number;
@@ -45,7 +46,7 @@ interface IAnnotationProps extends FieldViewProps {
     annoDoc: Doc;
     containerDataDoc: Doc;
     fieldKey: string;
-    pointerEvents?: () => Opt<string>;
+    pointerEvents?: () => Opt<Property.PointerEvents>;
 }
 @observer
 export class Annotation extends ObservableReactComponent<IAnnotationProps> {
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 4679941fb..aa10dcead 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -9,7 +9,7 @@ import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
 import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
 import { InkData, InkField, InkTool } from '../../../fields/InkField';
-import { SVGToBezier } from '../../util/bezierFit';
+import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { INode, parse } from 'svgson';
 import { Slider, Switch } from '@mui/material';
 import { Doc, DocListCast } from '../../../fields/Doc';
@@ -279,14 +279,14 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
             // eslint-disable-next-line @typescript-eslint/no-explicit-any
-            svgStrokes.forEach((child) => {
-                const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
+            svgStrokes.forEach(child => {
+                const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
                 strokeData.push([
                     convertedBezier.map(point => {
                         return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
                     }),
-                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : undefined,
-                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
+                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '',
+                    (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
                 ]);
             });
             return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
-- 
cgit v1.2.3-70-g09d2


From 4dd32452ad45ce0ed01fe577eb198a99ccd69754 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Aug 2024 22:38:53 -0400
Subject: cleaning up smartDrawHandler

---
 src/client/util/CurrentUserUtils.ts                |   2 -
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 488 ++++++++-------------
 3 files changed, 196 insertions(+), 296 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index cdf5505f8..ec8898b93 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -386,7 +386,6 @@ pie title Minerals in my tap water
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts),   opts: { _width: 300, _height: 300 }},
             {key: "Chat",       creator: Docs.Create.ChatDocument,                          opts: { _width: 300, _height: 300, }},
-            {key: "AnnoPalette",creator: Docs.Create.AnnoPaletteDocument,                   opts: {_width: 300, _height: 300, _dropAction: dropActionType.move }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}},
             {key: "ViewSlide",  creator: slideView,                                         opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}},
             {key: "Trail",      creator: Docs.Create.PresDocument,                          opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
@@ -429,7 +428,6 @@ pie title Minerals in my tap water
             { toolTip: "Tap or drag to create a bullet slide",    title: "PPT Slide", icon: "person-chalkboard",   dragFactory: doc.emptySlide as Doc,       clickFactory: DocCast(doc.emptySlide),     openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a view slide",      title: "View Slide", icon: "address-card",       dragFactory: doc.emptyViewSlide as Doc,   clickFactory: DocCast(doc.emptyViewSlide), openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
             { toolTip: "Tap or drag to create a data note",       title: "DataNote", icon: "window-maximize",      dragFactory: doc.emptyHeader as Doc,      clickFactory: DocCast(doc.emptyHeader),    openFactoryAsDelegate: true,           funcs: { hidden: "IsNoviceMode()"} },
-            { toolTip: "Tap or drag to create an annotation palette",title: "Annotation Palette", icon: "palette", dragFactory: doc.emptyAnnoPalette as Doc, clickFactory: DocCast(doc.emptyAnnoPalette)},
             { toolTip: "Toggle a Calculator REPL",                title: "replviewer",  icon: "calculator",                                      clickFactory: '<ScriptingRepl />' as unknown as Doc, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
            // { toolTip: "Toggle an UndoStack",                     title: "undostacker", icon: "calculator",                                      clickFactory: "<UndoStack />" as any,        openFactoryLocation: OpenWhere.overlay},
           ].map(tuple => (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c0f01383d..76549a423 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -2006,7 +2006,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 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();
+                !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
             }),
             icon: 'pen-to-square',
         });
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index aa10dcead..59d362d26 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -1,29 +1,29 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button, IconButton } from 'browndash-components';
 import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import React from 'react';
-import { SettingsManager } from '../../util/SettingsManager';
-import { ObservableReactComponent } from '../ObservableReactComponent';
-import { Button, IconButton } from 'browndash-components';
-import ReactLoading from 'react-loading';
 import { AiOutlineSend } from 'react-icons/ai';
-import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT';
-import { InkData, InkField, InkTool } from '../../../fields/InkField';
-import { SVGToBezier, SVGType } from '../../util/bezierFit';
+import ReactLoading from 'react-loading';
 import { INode, parse } from 'svgson';
-import { Slider, Switch } from '@mui/material';
+import { unimplementedFunction } from '../../../Utils';
 import { Doc, DocListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import { InkData, InkField, InkTool } from '../../../fields/InkField';
 import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
-import './SmartDrawHandler.scss';
-import { unimplementedFunction } from '../../../Utils';
+import { ImageField } from '../../../fields/URLField';
+import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
-import { MarqueeView } from '../collections/collectionFreeForm';
-import { ImageField, URLField } from '../../../fields/URLField';
-import { CollectionCardView } from '../collections/CollectionCardDeckView';
+import { SettingsManager } from '../../util/SettingsManager';
+import { undoable } from '../../util/UndoManager';
+import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { InkingStroke } from '../InkingStroke';
-import { undoBatch } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { CollectionCardView } from '../collections/CollectionCardDeckView';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import './SmartDrawHandler.scss';
 
 export interface DrawingOptions {
     text: string;
@@ -35,10 +35,14 @@ export interface DrawingOptions {
 }
 
 @observer
-// eslint-disable-next-line @typescript-eslint/no-empty-object-type
-export class SmartDrawHandler extends ObservableReactComponent<{}> {
+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 _selectedDoc: Doc | undefined = undefined;
+    private _errorOccurredOnce = false;
+
     @observable private _display: boolean = false;
     @observable private _pageX: number = 0;
     @observable private _pageY: number = 0;
@@ -47,18 +51,21 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @observable private _userInput: string = '';
     @observable private _showOptions: boolean = false;
     @observable private _showEditBox: boolean = false;
-    @observable public _showRegenerate: boolean = false;
     @observable private _complexity: number = 5;
     @observable private _size: number = 200;
     @observable private _autoColor: boolean = true;
     @observable private _regenInput: string = '';
     @observable private _canInteract: boolean = true;
 
-    private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
-    private _lastResponse: string = '';
-    private _selectedDoc: Doc | undefined = undefined;
-    private _errorOccurredOnce = false;
+    @observable public ShowRegenerate: boolean = false;
+
+    constructor(props: object) {
+        super(props);
+        makeObservable(this);
+        SmartDrawHandler.Instance = this;
+    }
 
+    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
     public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction;
     public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
         const drawing: Doc[] = [];
@@ -85,67 +92,21 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             drawing.push(inkDoc);
         });
 
-        const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
-        return collection;
-    };
-    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
-
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    constructor(props: any) {
-        super(props);
-        makeObservable(this);
-        SmartDrawHandler.Instance = this;
-    }
-
-    @action
-    setUserInput = (input: string) => {
-        if (this._canInteract) this._userInput = input;
-    };
-
-    @action
-    setRegenInput = (input: string) => {
-        if (this._canInteract) this._regenInput = input;
-    };
-
-    @action
-    setShowOptions = () => {
-        this._showOptions = !this._showOptions;
-    };
-
-    @action
-    setComplexity = (val: number) => {
-        if (this._canInteract) this._complexity = val;
-    };
-
-    @action
-    setSize = (val: number) => {
-        if (this._canInteract) this._size = val;
-    };
-
-    @action
-    setAutoColor = () => {
-        if (this._canInteract) this._autoColor = !this._autoColor;
-    };
-
-    @action
-    setEdit = () => {
-        this._showEditBox = !this._showEditBox;
+        return MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
     };
 
     @action
     displaySmartDrawHandler = (x: number, y: number) => {
-        this._pageX = x;
-        this._pageY = y;
+        [this._pageX, this._pageY] = [x, y];
         this._display = true;
     };
 
     @action
     displayRegenerate = (x: number, y: number) => {
         this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
-        this._pageX = x;
-        this._pageY = y;
+        [this._pageX, this._pageY] = [x, y];
         this._display = false;
-        this._showRegenerate = true;
+        this.ShowRegenerate = true;
         this._showEditBox = false;
         const docData = this._selectedDoc[DocData];
         this._lastResponse = StrCast(docData.drawingData);
@@ -154,7 +115,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
 
     @action
     hideSmartDrawHandler = () => {
-        this._showRegenerate = false;
+        this.ShowRegenerate = false;
         this._display = false;
         this._isLoading = false;
         this._showOptions = false;
@@ -168,17 +129,16 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     @action
     hideRegenerate = () => {
         if (!this._isLoading) {
-            this._showRegenerate = false;
+            this.ShowRegenerate = false;
             this._isLoading = false;
             this._regenInput = '';
             this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
         }
     };
 
-    @action
-    handleKeyPress = async (event: React.KeyboardEvent) => {
+    handleKeyPress = (event: React.KeyboardEvent) => {
         if (event.key === 'Enter') {
-            await this.handleSendClick();
+            this.handleSendClick();
         }
     };
 
@@ -186,7 +146,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     handleSendClick = async () => {
         this._isLoading = true;
         this._canInteract = false;
-        if (this._showRegenerate) {
+        if (this.ShowRegenerate) {
             await this.regenerate();
             this._regenInput = '';
             this._showEditBox = false;
@@ -195,7 +155,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             try {
                 await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
                 this.hideSmartDrawHandler();
-                this._showRegenerate = true;
+                this.ShowRegenerate = true;
             } catch (err) {
                 if (this._errorOccurredOnce) {
                     console.error('GPT call failed', err);
@@ -222,10 +182,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             console.error('GPT call failed');
             return;
         }
-        console.log(res);
         const strokeData = await this.parseSvg(res, startPt, false, autoColor);
         const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
-        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
         drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
 
         this._errorOccurredOnce = false;
@@ -254,12 +212,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 console.error('GPT call failed');
                 return;
             }
-            console.log(res);
             const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
-            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
             this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(this._selectedDoc);
             const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
-            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
             drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
             return strokeData;
         } catch (err) {
@@ -278,13 +233,10 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             const svgObject = await parse(svg[0]);
             const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
-            // eslint-disable-next-line @typescript-eslint/no-explicit-any
             svgStrokes.forEach(child => {
                 const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
                 strokeData.push([
-                    convertedBezier.map(point => {
-                        return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
-                    }),
+                    convertedBezier.map(point => ({ X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 })),
                     (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '',
                     (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
                 ]);
@@ -298,7 +250,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
      */
     colorWithGPT = async (drawing: Doc) => {
         const img = await this.getIcon(drawing);
-        const { href } = (img as URLField).url;
+        const { href } = ImageCast(img).url;
         const hrefParts = href.split('.');
         const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
         try {
@@ -308,20 +260,9 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
             strokes.forEach((stroke, i) => {
                 const inkingStroke = DocumentView.getDocumentView(stroke)?.ComponentView as InkingStroke;
                 const { inkData } = inkingStroke.inkScaledData();
-                coords.push(
-                    `${i + 1}. ${inkData
-                        .filter((point, index) => {
-                            return index % 4 === 0 || index == inkData.length - 1;
-                        })
-                        .map(point => {
-                            return `(${point.X.toString()}, ${point.Y.toString()})`;
-                        })}`
-                );
+                coords.push(`${i + 1}. ${inkData.filter((point, index) => index % 4 === 0 || index == inkData.length - 1).map(point => `(${point.X.toString()}, ${point.Y.toString()})`)}`);
             });
-            const response = await gptDrawingColor(hrefBase64, coords);
-            console.log(response);
-            const colorResponse = await gptAPICall(response, GPTCallType.COLOR, undefined);
-            console.log(colorResponse);
+            const colorResponse = await gptDrawingColor(hrefBase64, coords).then(response => gptAPICall(response, GPTCallType.COLOR, undefined));
             this.colorStrokes(colorResponse, drawing);
         } catch (error) {
             console.log('GPT call failed', error);
@@ -331,8 +272,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
     /**
      * Function that parses the GPT color response and sets the selected stroke(s) to the new color.
      */
-    @undoBatch
-    colorStrokes = (res: string, drawing: Doc) => {
+    colorStrokes = undoable((res: string, drawing: Doc) => {
         const colorList = res.match(/\{.*?\}/g);
         const strokes = DocListCast(drawing[DocData].data);
         colorList?.forEach((colors, index) => {
@@ -341,213 +281,175 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> {
                 strokes[index][DocData].color = strokeAndFill[0];
                 const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
                 const { inkData } = inkStroke.inkScaledData();
-                // eslint-disable-next-line @typescript-eslint/no-unused-expressions
                 InkingStroke.IsClosed(inkData) ? (strokes[index][DocData].fillColor = strokeAndFill[1]) : (strokes[index][DocData].fillColor = undefined);
             }
         });
-    };
+    }, 'color strokes');
 
     /**
      * Gets an image snapshot of a doc. In this class, it's used to snapshot a selected ink stroke/group to use for GPT color.
      */
     async getIcon(doc: Doc) {
         const docView = DocumentView.getDocumentView(doc);
-        console.log(doc);
         if (docView) {
-            console.log(docView);
             docView.ComponentView?.updateIcon?.();
             return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
         }
         return undefined;
     }
 
-    render() {
-        if (this._display) {
-            return (
-                <div
-                    id="label-handler"
-                    className="smart-draw-handler"
-                    style={{
-                        display: this._display ? '' : 'none',
-                        left: this._pageX,
-                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
-                        background: SettingsManager.userBackgroundColor,
-                        color: SettingsManager.userColor,
-                    }}>
-                    <div>
-                        <IconButton
-                            tooltip={'Cancel'}
-                            onClick={() => {
-                                this.hideSmartDrawHandler();
-                                this.hideRegenerate();
-                            }}
-                            icon={<FontAwesomeIcon icon="xmark" />}
-                            color={SettingsManager.userColor}
-                            style={{ width: '19px' }}
-                        />
-                        <input
-                            aria-label="Smart Draw Input"
-                            className="smartdraw-input"
-                            id="smartdraw-input"
-                            type="text"
-                            style={{ color: 'black' }}
-                            value={this._userInput}
-                            onChange={e => {
-                                this.setUserInput(e.target.value);
-                            }}
-                            placeholder="Enter item to draw"
-                            onKeyDown={this.handleKeyPress}
-                        />
-                        <IconButton tooltip="Advanced Options" icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} color={SettingsManager.userColor} style={{ width: '14px' }} onClick={this.setShowOptions} />
-                        <Button
-                            style={{ alignSelf: 'flex-end' }}
-                            text="Send"
-                            icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                            iconPlacement="right"
-                            color={SettingsManager.userColor}
-                            onClick={this.handleSendClick}
-                        />
-                    </div>
-                    {this._showOptions && (
-                        <>
-                            <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
-                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
-                                    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={this.setAutoColor}
-                                    />
-                                </div>
-                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
-                                    Complexity
-                                    <Slider
-                                        sx={{
-                                            '& .MuiSlider-thumb': {
-                                                color: SettingsManager.userColor,
-                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
-                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
-                                                },
-                                            },
-                                            '& .MuiSlider-track': {
-                                                color: SettingsManager.userVariantColor,
-                                            },
-                                            '& .MuiSlider-rail': {
-                                                color: SettingsManager.userColor,
-                                            },
-                                        }}
-                                        style={{ width: '80%' }}
-                                        min={1}
-                                        max={10}
-                                        step={1}
-                                        size="small"
-                                        value={this._complexity}
-                                        onChange={(e, val) => {
-                                            this.setComplexity(val as number);
-                                        }}
-                                        valueLabelDisplay="auto"
-                                    />
-                                </div>
-                                <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
-                                    Size (in pixels)
-                                    <Slider
-                                        sx={{
-                                            '& .MuiSlider-thumb': {
-                                                color: SettingsManager.userColor,
-                                                '&.Mui-focusVisible, &:hover, &.Mui-active': {
-                                                    boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
-                                                },
-                                            },
-                                            '& .MuiSlider-track': {
-                                                color: SettingsManager.userVariantColor,
-                                            },
-                                            '& .MuiSlider-rail': {
-                                                color: SettingsManager.userColor,
-                                            },
-                                        }}
-                                        style={{ width: '80%' }}
-                                        min={50}
-                                        max={700}
-                                        step={10}
-                                        size="small"
-                                        value={this._size}
-                                        onChange={(e, val) => {
-                                            this.setSize(val as number);
-                                        }}
-                                        valueLabelDisplay="auto"
-                                    />
-                                </div>
-                            </div>
-                        </>
-                    )}
+    renderDisplay() {
+        return (
+            <div
+                id="label-handler"
+                className="smart-draw-handler"
+                style={{
+                    display: this._display ? '' : 'none',
+                    left: this._pageX,
+                    ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+                    background: SettingsManager.userBackgroundColor,
+                    color: SettingsManager.userColor,
+                }}>
+                <div>
+                    <IconButton
+                        tooltip="Cancel"
+                        onClick={() => {
+                            this.hideSmartDrawHandler();
+                            this.hideRegenerate();
+                        }}
+                        icon={<FontAwesomeIcon icon="xmark" />}
+                        color={SettingsManager.userColor}
+                        style={{ width: '19px' }}
+                    />
+                    <input
+                        aria-label="Smart Draw Input"
+                        className="smartdraw-input"
+                        type="text"
+                        style={{ color: 'black' }}
+                        value={this._userInput}
+                        onChange={action(e => this._canInteract && (this._userInput = e.target.value))}
+                        placeholder="Enter item to draw"
+                        onKeyDown={this.handleKeyPress}
+                    />
+                    <IconButton
+                        tooltip="Advanced Options"
+                        icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />}
+                        color={SettingsManager.userColor}
+                        style={{ width: '14px' }}
+                        onClick={action(() => (this._showOptions = !this._showOptions))}
+                    />
+                    <Button
+                        style={{ alignSelf: 'flex-end' }}
+                        text="Send"
+                        icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                        iconPlacement="right"
+                        color={SettingsManager.userColor}
+                        onClick={this.handleSendClick}
+                    />
                 </div>
-            );
-        } else if (this._showRegenerate) {
-            return (
-                <div
-                    id="smartdraw-options-menu"
-                    className="smart-draw-handler"
-                    style={{
-                        left: this._pageX,
-                        ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
-                        background: SettingsManager.userBackgroundColor,
-                        color: SettingsManager.userColor,
-                    }}>
-                    <div
-                        style={{
-                            display: 'flex',
-                            flexDirection: 'row',
-                        }}>
-                        <IconButton
-                            tooltip="Regenerate"
-                            icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
-                            color={SettingsManager.userColor}
-                            onClick={this.handleSendClick}
-                        />
-                        <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.setEdit} />
-                        {this._showEditBox && (
-                            <div
-                                style={{
-                                    display: 'flex',
-                                    flexDirection: 'row',
-                                }}>
-                                <input
-                                    aria-label="Edit instructions input"
-                                    className="smartdraw-input"
-                                    id="regen-input"
-                                    type="text"
-                                    style={{ color: 'black' }}
-                                    value={this._regenInput}
-                                    onChange={e => {
-                                        this.setRegenInput(e.target.value);
-                                    }}
-                                    onKeyDown={this.handleKeyPress}
-                                    placeholder="Edit instructions"
-                                />
-                                <Button
-                                    style={{ alignSelf: 'flex-end' }}
-                                    text="Send"
-                                    icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
-                                    iconPlacement="right"
-                                    color={SettingsManager.userColor}
-                                    onClick={this.handleSendClick}
-                                />
-                            </div>
-                        )}
+                {this._showOptions && (
+                    <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
+                        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
+                            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(e => this._canInteract && (this._autoColor = !this._autoColor))}
+                            />
+                        </div>
+                        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
+                            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 style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
+                            Size (in pixels)
+                            <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` } },
+                                }}
+                                style={{ width: '80%' }}
+                                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>
+        );
+    }
+
+    renderRegenerate() {
+        return (
+            <div
+                className="smart-draw-handler"
+                style={{
+                    left: this._pageX,
+                    ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+                    background: SettingsManager.userBackgroundColor,
+                    color: SettingsManager.userColor,
+                }}>
+                <div style={{ display: 'flex', flexDirection: 'row' }}>
+                    <IconButton
+                        tooltip="Regenerate"
+                        icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+                        color={SettingsManager.userColor}
+                        onClick={this.handleSendClick}
+                    />
+                    <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} />
+                    {this._showEditBox && (
+                        <div style={{ display: 'flex', flexDirection: 'row' }}>
+                            <input
+                                aria-label="Edit instructions input"
+                                className="smartdraw-input"
+                                type="text"
+                                style={{ color: 'black' }}
+                                value={this._regenInput}
+                                onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
+                                onKeyDown={this.handleKeyPress}
+                                placeholder="Edit instructions"
+                            />
+                            <Button
+                                style={{ alignSelf: 'flex-end' }}
+                                text="Send"
+                                icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+                                iconPlacement="right"
+                                color={SettingsManager.userColor}
+                                onClick={this.handleSendClick}
+                            />
+                        </div>
+                    )}
                 </div>
-            );
-        } else {
-            return <></>;
-        }
+            </div>
+        );
+    }
+
+    render() {
+        return this._display ? this.renderDisplay() : this.ShowRegenerate ? this.renderRegenerate() : null;
     }
 }
-- 
cgit v1.2.3-70-g09d2


From d1fad65d5f3bfb2ed7d2b94af2c3c609a2090545 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 29 Aug 2024 23:38:12 -0400
Subject: made smartDraw autoFocus and be available from : menu

---
 src/client/documents/DocUtils.ts                                   | 7 +++++++
 src/client/views/ContextMenuItem.tsx                               | 2 +-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx      | 6 +++---
 src/client/views/smartdraw/SmartDrawHandler.tsx                    | 1 +
 4 files changed, 12 insertions(+), 4 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 30b71a09b..dcb5e1fe3 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -34,6 +34,8 @@ import { OpenWhere } from '../views/nodes/OpenWhere';
 import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import { DocumentType } from './DocumentTypes';
 import { Docs, DocumentOptions } from './Documents';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
 const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
@@ -378,6 +380,11 @@ export namespace DocUtils {
                 }, StrCast(dragDoc.title)),
                 icon: Doc.toIcon(dragDoc),
             })) as ContextMenuProps[];
+        documentList.push({
+            description: ':Smart Drawing',
+            event: e => (DocumentView.Selected().lastElement().ComponentView as CollectionFreeFormView)?.showSmartDraw(e?.x || 0, e?.y || 0),
+            icon: 'file',
+        });
         ContextMenu.Instance.addItem({
             description: 'Create document',
             subitems: documentList,
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 5b4eb704b..5d31173e1 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -18,7 +18,7 @@ export interface ContextMenuProps {
     noexpand?: boolean; // whether to render the submenu items as a flyout from this item, or inline in place of this item
 
     undoable?: boolean; // whether to wrap the event callback in an UndoBatch or not
-    event?: (stuff?: unknown) => void;
+    event?: (stuff?: { x: number; y: number }) => void;
 }
 
 @observer
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 76549a423..880d1cd74 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -510,7 +510,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation();
                         break;
                     case InkTool.SmartDraw:
-                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.showSmartDraw, hit !== -1);
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, e => this.showSmartDraw(e.pageX, e.pageY), hit !== -1);
                         e.stopPropagation();
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
@@ -1229,11 +1229,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    showSmartDraw = (e: PointerEvent) => {
+    showSmartDraw = (x: number, y: number) => {
         SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
         SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
         SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
-        SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY);
+        SmartDrawHandler.Instance.displaySmartDrawHandler(x, y);
     };
 
     _drawing: Doc[] = [];
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 59d362d26..879358742 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -325,6 +325,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         aria-label="Smart Draw Input"
                         className="smartdraw-input"
                         type="text"
+                        autoFocus
                         style={{ color: 'black' }}
                         value={this._userInput}
                         onChange={action(e => this._canInteract && (this._userInput = e.target.value))}
-- 
cgit v1.2.3-70-g09d2


From b453414dc2bb11d56c5dcb2a73d389eefabff7d5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 2 Sep 2024 12:16:43 -0400
Subject: cleaned up updateIcon to use promises to avoid a bunch of setTimeouts

---
 src/client/util/DropConverter.ts                   |  3 +-
 src/client/views/InkTranscription.tsx              | 14 ++------
 src/client/views/ViewBoxInterface.ts               |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 13 ++++++-
 src/client/views/nodes/PDFBox.tsx                  | 42 +++++++++++-----------
 src/client/views/nodes/VideoBox.tsx                | 15 ++++----
 src/client/views/nodes/WebBox.tsx                  | 29 ++++++---------
 src/client/views/smartdraw/AnnotationPalette.tsx   | 40 +++++++--------------
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 14 +-------
 10 files changed, 71 insertions(+), 103 deletions(-)

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

diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 57c79cff3..0ede44298 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -92,7 +92,8 @@ export function makeUserTemplateButton(doc: Doc) {
  * Similar to makeUserTemplateButton, but rather than creating a draggable button for the template, it takes in
  * an ImageField that will display.
  */
-export function makeUserTemplateImage(doc: Doc, image: ImageField) {
+export function makeUserTemplateImage(doc: Doc, imageHref: string | undefined) {
+    const image = imageHref ?? 'http://www.cs.brown.edu/~bcz/noImage.png';
     const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
     if (layoutDoc.type !== DocumentType.FONTICON) {
         !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 485a0e7c4..26e0aa372 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -3,7 +3,7 @@ import { action, observable } from 'mobx';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { InkData, InkField, InkTool } from '../../fields/InkField';
-import { Cast, DateCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { Cast, DateCast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
 import { aggregateBounds } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm';
@@ -273,7 +273,7 @@ export class InkTranscription extends React.Component {
                     DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.();
                     this.currGroup.transcription = text;
                     this.currGroup.title = text;
-                    let image = await this.getIcon();
+                    let image = await DocumentView.GetDocImage(this.currGroup);
                     const pathname = image?.url.href as string;
                     console.log(image?.url);
                     console.log(image);
@@ -300,16 +300,6 @@ export class InkTranscription extends React.Component {
             }
         }
     };
-    async getIcon() {
-        const docView = DocumentView.getDocumentView(this.currGroup);
-        console.log(this.currGroup);
-        if (docView) {
-            console.log(docView);
-            docView.ComponentView?.updateIcon?.();
-            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
-        }
-        return undefined;
-    }
     imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
         try {
             const response = await fetch(imageUrl);
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index 111771ba0..f66f6062e 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -22,7 +22,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
         return ''; //
     }
     promoteCollection?: () => void; // moves contents of collection to parent
-    updateIcon?: (usePanelDimensions?: boolean) => void; // updates the icon representation of the document
+    updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
     restoreView?: (viewSpec: Doc) => boolean;
     scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 880d1cd74..4b4a07757 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1878,7 +1878,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     updateIcon = (usePanelDimensions?: boolean) =>
         UpdateIcon(
-            this.layoutDoc[Id] + '-icon' + new Date().getTime(),
+            this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
             this.DocumentView?.().ContentDiv!,
             usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width),
             usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height),
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4cc7eb0ff..c279badf4 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -16,7 +16,7 @@ import { List } from '../../../fields/List';
 import { PrefetchProxy } from '../../../fields/Proxy';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { AudioAnnoState } from '../../../server/SharedMediaTypes';
@@ -1087,6 +1087,17 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
      * Pins a Doc to the current presentation trail. (see TabDocView for implementation)
      */
     public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void;
+
+    /**
+     * Renders an image of a Doc into the Doc's icon field, then returns a promise for the image value
+     * @param doc Doc to snapshot
+     * @returns promise of icon ImageField
+     */
+    public static GetDocImage(doc: Doc) {
+        return DocumentView.getDocumentView(doc)
+            ?.ComponentView?.updateIcon?.()
+            .then(() => ImageCast(DocCast(doc).icon));
+    }
     /**
      * The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event).  Used by GestureOverlay to determine the doc a gesture should apply to.
      */
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b17275a1e..209c5abbc 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -179,28 +179,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     updateIcon = () => {
         // currently we render pdf icons as text labels
         const docViewContent = this.DocumentView?.().ContentDiv;
-        const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime();
-        this._pdfViewer?._mainCont.current &&
-            docViewContent &&
-            UpdateIcon(
-                filename,
-                docViewContent,
-                NumCast(this.layoutDoc._width),
-                NumCast(this.layoutDoc._height),
-                this._props.PanelWidth(),
-                this._props.PanelHeight(),
-                NumCast(this.layoutDoc._layout_scrollTop),
-                NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1),
-                true,
-                this.layoutDoc[Id] + '-icon',
-                (iconFile: string, nativeWidth: number, nativeHeight: number) => {
-                    setTimeout(() => {
-                        this.dataDoc.icon = new ImageField(iconFile);
-                        this.dataDoc.icon_nativeWidth = nativeWidth;
-                        this.dataDoc.icon_nativeHeight = nativeHeight;
-                    }, 500);
-                }
-            );
+        const filename = this.layoutDoc[Id] + '_icon_' + new Date().getTime();
+        return !(this._pdfViewer?._mainCont.current && docViewContent)
+            ? new Promise<void>(res => res())
+            : UpdateIcon(
+                  filename,
+                  docViewContent,
+                  NumCast(this.layoutDoc._width),
+                  NumCast(this.layoutDoc._height),
+                  this._props.PanelWidth(),
+                  this._props.PanelHeight(),
+                  NumCast(this.layoutDoc._layout_scrollTop),
+                  NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1),
+                  true,
+                  this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
+                  (iconFile: string, nativeWidth: number, nativeHeight: number) => {
+                      this.dataDoc.icon = new ImageField(iconFile);
+                      this.dataDoc.icon_nativeWidth = nativeWidth;
+                      this.dataDoc.icon_nativeHeight = nativeHeight;
+                  }
+              );
     };
 
     componentWillUnmount() {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4933869a7..d653b27d7 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -298,18 +298,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, '');
             const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_'));
             const filename = basename(encodedFilename);
-            ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+            return ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => {
+                if (returnedFilename) (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY);
+            });
         }
+        return new Promise<void>(res => res());
     };
 
-    updateIcon = () => {
-        const makeIcon = (returnedfilename: string) => {
+    updateIcon = () =>
+        this.Snapshot(undefined, undefined, (returnedfilename: string) => {
             this.dataDoc.icon = new ImageField(returnedfilename);
             this.dataDoc.icon_nativeWidth = NumCast(this.layoutDoc._width);
             this.dataDoc.icon_nativeHeight = NumCast(this.layoutDoc._height);
-        };
-        this.Snapshot(undefined, undefined, makeIcon);
-    };
+        });
 
     // creates link for snapshot
     createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
@@ -459,7 +460,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             const url = field.url.href;
             const subitems: ContextMenuProps[] = [];
             subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' });
-            subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' });
+            subitems.push({ description: 'Take Snapshot', event: () => this.Snapshot(), icon: 'expand-arrows-alt' });
             this.Document.type === DocumentType.SCREENSHOT &&
                 subitems.push({
                     description: 'Screen Capture',
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 8088bddd4..a5788d02a 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -144,38 +144,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     updateIcon = async () => {
-        if (!this._iframe) return;
+        if (!this._iframe) return new Promise<void>(res => res());
         const scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
         const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
         const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth();
         let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
         if (!htmlString) {
-            htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text();
+            htmlString = await fetch(ClientUtils.CorsProxy(this.webField!.href)).then(response => response.text());
         }
         this.layoutDoc.thumb = undefined;
         this.Document.thumbLockout = true; // lock to prevent multiple thumb updates.
-        CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
+        return (CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) as Promise<string>)
             .then((dataUrl: string) => {
                 if (dataUrl.includes('<!DOCTYPE')) {
                     console.log('BAD DATA IN THUMB CREATION');
                     return;
                 }
-                ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
-                    setTimeout(
-                        action(() => {
-                            this.Document.thumbLockout = false;
-                            this.layoutDoc.thumb = new ImageField(returnedfilename);
-                            this.layoutDoc.thumbScrollTop = scrollTop;
-                            this.layoutDoc.thumbNativeWidth = nativeWidth;
-                            this.layoutDoc.thumbNativeHeight = nativeHeight;
-                        }),
-                        500
-                    )
-                );
+                return ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '_icon_' + new Date().getTime(), true, this.layoutDoc[Id] + '_icon_').then(returnedfilename => {
+                    this.Document.thumbLockout = false;
+                    this.layoutDoc.thumb = new ImageField(returnedfilename);
+                    this.layoutDoc.thumbScrollTop = scrollTop;
+                    this.layoutDoc.thumbNativeWidth = nativeWidth;
+                    this.layoutDoc.thumbNativeHeight = nativeHeight;
+                });
             })
-            .catch((error: object) => {
-                console.error('oops, something went wrong!', error);
-            });
+            .catch((error: object) => console.error('oops, something went wrong!', error));
     };
 
     componentDidMount() {
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 72364be09..2ebe5bca5 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
 import ReactLoading from 'react-loading';
-import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
@@ -25,6 +25,7 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
 import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import { setTime } from 'react-datepicker/dist/date_utils';
 
 interface AnnotationPaletteProps {
     Document: Doc;
@@ -109,26 +110,20 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
      */
     public static addToPalette = async (doc: Doc) => {
         if (!doc.savedAsAnno) {
-            const clone = await Doc.MakeClone(doc);
-            clone.clone.title = doc.title;
-            const image = ((await AnnotationPalette.getIcon(doc)) ?? ImageCast(doc[Doc.LayoutFieldKey(doc)]))?.[Copy]();
-            if (image) {
-                const imageTemplate = makeUserTemplateImage(clone.clone, image);
-                Doc.AddDocToList(Doc.MyAnnos, 'data', imageTemplate);
-                doc.savedAsAnno = true;
-            }
+            Doc.MakeClone(doc).then(cloneMap =>
+                DocumentView.getDocumentView(doc)
+                    ?.ComponentView?.updateIcon?.(true)
+                    .then(() => {
+                        const { clone } = cloneMap;
+                        clone.title = doc.title;
+                        const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
+                        Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image));
+                        doc.savedAsAnno = true;
+                    })
+            );
         }
     };
 
-    public static getIcon(group: Doc) {
-        const docView = DocumentView.getDocumentView(group);
-        if (docView) {
-            docView.ComponentView?.updateIcon?.(true);
-            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
-        }
-        return undefined;
-    }
-
     /**
      * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
      * the annotation palette.
@@ -184,15 +179,6 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
         this.resetPalette(true);
     };
 
-    async getIcon(group: Doc) {
-        const docView = DocumentView.getDocumentView(group);
-        if (docView) {
-            docView.ComponentView?.updateIcon?.(true);
-            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
-        }
-        return undefined;
-    }
-
     render() {
         return (
             <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}>
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 879358742..8271c0959 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -249,7 +249,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * Sends request to GPT API to recolor a selected ink document or group of ink documents.
      */
     colorWithGPT = async (drawing: Doc) => {
-        const img = await this.getIcon(drawing);
+        const img = await DocumentView.GetDocImage(drawing);
         const { href } = ImageCast(img).url;
         const hrefParts = href.split('.');
         const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
@@ -286,18 +286,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         });
     }, 'color strokes');
 
-    /**
-     * Gets an image snapshot of a doc. In this class, it's used to snapshot a selected ink stroke/group to use for GPT color.
-     */
-    async getIcon(doc: Doc) {
-        const docView = DocumentView.getDocumentView(doc);
-        if (docView) {
-            docView.ComponentView?.updateIcon?.();
-            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
-        }
-        return undefined;
-    }
-
     renderDisplay() {
         return (
             <div
-- 
cgit v1.2.3-70-g09d2


From 692076b1356309111c4f2cb69cbdbf4be1a825bd Mon Sep 17 00:00:00 2001
From: eleanor-park <eleanor_park@brown.edu>
Date: Sun, 22 Sep 2024 15:40:19 -0400
Subject: small bug fixes for smart draw

---
 src/client/util/Scripting.ts                       |  4 +-
 src/client/util/bezierFit.ts                       |  4 +-
 src/client/views/DocumentButtonBar.tsx             | 15 ----
 src/client/views/InkStrokeProperties.ts            | 35 ++++------
 src/client/views/PropertiesView.tsx                |  6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 16 +++--
 src/client/views/smartdraw/AnnotationPalette.scss  | 46 +++++++++++++
 src/client/views/smartdraw/AnnotationPalette.tsx   | 56 ++++++++++-----
 src/client/views/smartdraw/SmartDrawHandler.scss   | 41 +++++++++++
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 80 ++++++++++++++++------
 10 files changed, 213 insertions(+), 90 deletions(-)

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

diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c63d3d7cb..cb314e3f1 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,7 +1,7 @@
 // export const ts = (window as any).ts;
 // import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
 // import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
-import typescriptlib from 'type_decls.d';
+// import typescriptlib from 'type_decls.d';
 import * as ts from 'typescript';
 import { Doc, FieldType } from '../../fields/Doc';
 import { RefField } from '../../fields/RefField';
@@ -248,7 +248,7 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     const funcScript = `(function(${paramString})${reqTypes} { ${body} })`;
     host.writeFile('file.ts', funcScript);
 
-    if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
+    // if (typecheck) host.writeFile('node_modules/typescript/lib/lib.d.ts', typescriptlib);
     const program = ts.createProgram(['file.ts'], {}, host);
     const testResult = program.emit();
     const outputText = host.readFile('file.js');
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 4c7f4a0ba..4aef28e6b 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -696,7 +696,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             const matches: RegExpMatchArray[] = Array.from(
                 attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
             );
-            let lastPt: Point = { X: 0, Y: 0 };
+            let lastPt: Point = startPt;
             matches.forEach(match => {
                 if (match[0].startsWith('Q')) {
                     coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
@@ -711,7 +711,7 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
                     coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
                     lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) };
                 } else {
-                    coordList.push(lastPt || { X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+                    coordList.push(lastPt);
                     coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
                     coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
                     coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 096f058ad..04c1d359c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -239,20 +239,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
         );
     }
 
-    saveAnno = undoable(async (targetDoc: Doc) => await AnnotationPalette.addToPalette(targetDoc), 'save to palette');
-
-    @computed
-    get saveAnnoButton() {
-        const targetDoc = this.view0?.Document;
-        return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{targetDoc.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette'}</div>}>
-                <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => this.saveAnno(targetDoc)}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon={targetDoc.savedAsAnno ? 'clipboard-check' : 'file-arrow-down'} />
-                </div>
-            </Tooltip>
-        );
-    }
-
     @computed
     get shareButton() {
         const targetDoc = this.view0?.Document;
@@ -473,7 +459,6 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
                 <div className="documentButtonBar-button">{this.templateButton}</div>
                 {!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
-                <div className="documentButtonBar-button">{this.saveAnnoButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 <div className="documentButtonBar-button">{this.calendarButton}</div>
                 <div className="documentButtonBar-button">{this.keywordButton}</div>
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 13807c25f..5cacde0d4 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -499,31 +499,20 @@ export class InkStrokeProperties {
             const inkView = DocumentView.getDocumentView(inkDoc);
             const inkStroke = inkView?.ComponentView as InkingStroke;
             const { inkData } = inkStroke.inkScaledData();
-
-            const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]);
-            console.log(result);
-            if (result && (result.Name === 'line' ? result.Score > 0.92 : result.Score > 0.85)) {
-                switch (result.Name) {
-                    case Gestures.Line:
-                    case Gestures.Triangle:
-                    case Gestures.Rectangle:
-                    case Gestures.Circle:
-                        GestureOverlay.makeBezierPolygon(inkData, result.Name, true);
-                        break;
-                    default:
-                }
-            } else {
-                const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore
-                if (polylinePoints.length > 2) {
-                    const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore
-                    for (var i = 4; i < inkData.length - 3; i += 4) {
-                        const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y);
-                        if (!contains) {
-                            this._currentPoint = i;
-                            inkView && this.deletePoints(inkView, false);
-                        }
+            const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore
+            if (polylinePoints.length > 2) {
+                const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore
+                for (var i = 4; i < inkData.length - 3; i += 4) {
+                    const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y);
+                    if (!contains) {
+                        this._currentPoint = i;
+                        inkView && this.deletePoints(inkView, false);
                     }
                 }
+                // close the curve if the first and last points are really close (based on tolerance)
+                if (!InkingStroke.IsClosed(inkData) && Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) <= tolerance * 2) {
+                    inkData[inkData.length - 1] = inkData[0];
+                }
             }
         });
     }, 'smooth ink stroke');
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 9299705ee..39ed16f5d 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -991,12 +991,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
                         'Smooth Amount',
                         '',
                         1,
-                        Math.max(20, this.smoothAmt),
+                        Math.max(10, this.smoothAmt),
                         this.smoothAmt,
                         (val: number) => {
                             !isNaN(val) && (this.smoothAmt = val);
                         },
-                        20,
+                        10,
                         1
                     )}
                 </div>
@@ -1021,7 +1021,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
             doc[DocData].stroke_markerScale = Number(value);
         });
     }
-    @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '10'); } // prettier-ignore
+    @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore
     set smoothAmt(value) {
         this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value));
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4b4a07757..ad1223097 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -43,6 +43,7 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere';
 import { PinDocView, PinProps } from '../../PinFuncs';
+import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
 import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
 import { StyleProp } from '../../StyleProp';
 import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
@@ -1269,21 +1270,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this._drawing.push(inkDoc);
         });
         const collection = MarqueeView.getCollection(this._drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+        console.log('is error here');
         return collection;
     };
 
     /**
      * Part of regenerating a drawing--deletes the old drawing.
      */
-    removeDrawing = (doc?: Doc) => {
+    removeDrawing = (useLastContainer: boolean, doc?: Doc) => {
         this._batch = UndoManager.StartBatch('regenerateDrawing');
-        if (doc) {
+        if (useLastContainer && this._drawingContainer) {
+            this._props.removeDocument?.(this._drawingContainer);
+        } else if (doc) {
             const docData = doc[DocData];
             const children = DocListCast(docData.data);
             this._props.removeDocument?.(doc);
             this._props.removeDocument?.(children);
-        } else {
-            if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
         }
         this._drawing = [];
     };
@@ -1294,6 +1296,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
         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;
@@ -2010,6 +2013,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }),
             icon: 'pen-to-square',
         });
+        optionItems.push({
+            description: this.Document.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette',
+            event: action(undoable(async () => await AnnotationPalette.addToPalette(this.Document), 'save to palette')),
+            icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down',
+        });
         this._props.renderDepth &&
             optionItems.push({
                 description: 'Use Background Color as Default',
diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss
index 9f875f61a..4f11e8afc 100644
--- a/src/client/views/smartdraw/AnnotationPalette.scss
+++ b/src/client/views/smartdraw/AnnotationPalette.scss
@@ -8,3 +8,49 @@
     border-radius: 5px;
     margin: auto;
 }
+
+.palette-create {
+    display: flex;
+    flex-direction: row;
+    width: 170px;
+
+    .palette-create-input {
+        color: black;
+        width: 170px;
+    }
+}
+
+.palette-create-options {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+    width: 170px;
+    margin-top: 5px;
+
+    .palette-color {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 40px;
+    }
+
+    .palette-detail,
+    .palette-size {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        width: 60px;
+    }
+}
+
+.palette-buttons {
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+}
+
+.palette-save-reset {
+    display: flex;
+    flex-direction: row;
+}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index a2d6cc88d..0c8dbf12d 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -23,11 +23,22 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
 import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import { ImageField } from '../../../fields/URLField';
+import { Copy } from '../../../fields/FieldSymbols';
 
 interface AnnotationPaletteProps {
     Document: Doc;
 }
 
+/**
+ * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette
+ * is to offer an easy way for users to save then drag and drop repeated annotations onto a document.
+ * These annotations can be of any annotation type and operate similarly to user templates.
+ *
+ * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can
+ * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing
+ * to choose from. These drawings can then be saved to the palette as annotations.
+ */
 @observer
 export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> {
     @observable private _paletteMode: 'create' | 'view' = 'view';
@@ -107,20 +118,25 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
      */
     public static addToPalette = async (doc: Doc) => {
         if (!doc.savedAsAnno) {
-            Doc.MakeClone(doc).then(cloneMap =>
-                DocumentView.getDocumentView(doc)
-                    ?.ComponentView?.updateIcon?.(true)
-                    .then(() => {
-                        const { clone } = cloneMap;
-                        clone.title = doc.title;
-                        const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
-                        Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image));
-                        doc.savedAsAnno = true;
-                    })
-            );
+            const docView = DocumentView.getDocumentView(doc);
+            await docView?.ComponentView?.updateIcon?.(true);
+            const { clone } = await Doc.MakeClone(doc);
+            clone.title = doc.title;
+            const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
+            Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image));
+            doc.savedAsAnno = true;
         }
     };
 
+    public static getIcon(group: Doc) {
+        const docView = DocumentView.getDocumentView(group);
+        if (docView) {
+            docView.ComponentView?.updateIcon?.(true);
+            return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+        }
+        return undefined;
+    }
+
     /**
      * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
      * the annotation palette.
@@ -172,6 +188,8 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
         docData.drawingSize = this._opts.size;
         docData.drawingData = this._gptRes[cIndex];
         docData.width = this._opts.size;
+        docData.x = this._opts.x;
+        docData.y = this._opts.y;
         await AnnotationPalette.addToPalette(focusedDrawing);
         this.resetPalette(true);
     };
@@ -206,12 +224,12 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                 )}
                 {this._paletteMode === 'create' && (
                     <>
-                        <div style={{ display: 'flex', flexDirection: 'row', width: '170px' }}>
+                        <div className="palette-create">
                             <input
+                                className="palette-create-input"
                                 aria-label="label-input"
                                 id="new-label"
                                 type="text"
-                                style={{ color: 'black', width: '170px' }}
                                 value={this._userInput}
                                 onChange={e => {
                                     this.setUserInput(e.target.value);
@@ -228,8 +246,8 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                                 onClick={this.generateDrawings}
                             />
                         </div>
-                        <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}>
-                            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '40px' }}>
+                        <div className="palette-create-options">
+                            <div className="palette-color">
                                 Color
                                 <Switch
                                     sx={{
@@ -246,7 +264,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                                     onChange={() => this.setColor(!this._opts.autoColor)}
                                 />
                             </div>
-                            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
+                            <div className="palette-detail">
                                 Detail
                                 <Slider
                                     sx={{
@@ -275,7 +293,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                                     valueLabelDisplay="auto"
                                 />
                             </div>
-                            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
+                            <div className="palette-size">
                                 Size
                                 <Slider
                                     sx={{
@@ -325,9 +343,9 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                             childFiltersByRanges={returnEmptyFilter}
                             searchFilterDocs={returnEmptyDoclist}
                         />
-                        <div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
+                        <div className="palette-buttons">
                             <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
-                            <div style={{ display: 'flex', flexDirection: 'row' }}>
+                            <div className="palette-save-reset">
                                 <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} />
                                 <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} />
                             </div>
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
index 6d402a80f..0e8bd3349 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.scss
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -1,3 +1,44 @@
 .smart-draw-handler {
     position: absolute;
 }
+
+.smartdraw-input {
+    color: black;
+}
+
+.smartdraw-options {
+    display: flex;
+    flex-direction: row;
+    justify-content: space-around;
+
+    .auto-color {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        width: 30%;
+    }
+
+    .complexity {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        width: 31%;
+    }
+
+    .size {
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        width: 39%;
+
+        .size-slider {
+            width: 80%;
+        }
+    }
+}
+
+.regenerate-box,
+.edit-box {
+    display: flex;
+    flex-direction: row;
+}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 8271c0959..e362c0c89 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -12,7 +12,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 { ImageField } from '../../../fields/URLField';
 import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT';
 import { Docs } from '../../documents/Documents';
 import { SettingsManager } from '../../util/SettingsManager';
@@ -34,6 +33,21 @@ export interface DrawingOptions {
     y: number;
 }
 
+/**
+ * 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
+ * it will be colored. If the drawing is colored, GPT will automatically define the stroke and fill of each
+ * stroke. Drawings are retrieved from GPT as SVG code then converted into Dash-supported Beziers.
+ *
+ * The handler is selected from the ink tools menu. To generate a drawing, users can click anywhere on the freeform
+ * canvas and a popup will appear that prompts them to create a drawing. Once the drawing is created, users have
+ * the option to regenerate or edit the drawing.
+ *
+ * When each drawing is created, it is added to Dash as a group of ink strokes. The group is tagged with metadata
+ * for user input, the drawing's SVG code, and its settings (size, complexity). In the context menu -> 'Options',
+ * users can then show the drawing editor and regenerate/edit them at any point in the future.
+ */
+
 @observer
 export class SmartDrawHandler extends ObservableReactComponent<object> {
     static Instance: SmartDrawHandler;
@@ -65,8 +79,19 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         SmartDrawHandler.Instance = this;
     }
 
+    /**
+     *  AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e. 
+        CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added
+        or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.)
+    */
     public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
-    public RemoveDrawing: (doc?: Doc) => 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,
+     * creates ink documents for each stroke, then adds the strokes to a collection. This can also be redefined by other
+     * 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 CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
         const drawing: Doc[] = [];
         strokeList.forEach((stroke: [InkData, string, string]) => {
@@ -101,6 +126,11 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._display = true;
     };
 
+    /**
+     * This is called in two places: 1. In this class, where the regenerate popup shows as soon as a
+     * drawing is created to replace the original smart draw popup. 2. From the context menu to make
+     * the regenerate popup show by user command.
+     */
     @action
     displayRegenerate = (x: number, y: number) => {
         this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
@@ -113,6 +143,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
     };
 
+    /**
+     * Hides the smart draw handler and resets its fields to their default.
+     */
     @action
     hideSmartDrawHandler = () => {
         this.ShowRegenerate = false;
@@ -126,6 +159,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         Doc.ActiveTool = InkTool.None;
     };
 
+    /**
+     * Hides the popup that allows users to regenerate a drawing and resets its corresponding fields.
+     */
     @action
     hideRegenerate = () => {
         if (!this._isLoading) {
@@ -136,12 +172,20 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         }
     };
 
+    /**
+     * This allows users to press the return/enter key to send input.
+     */
     handleKeyPress = (event: React.KeyboardEvent) => {
         if (event.key === 'Enter') {
             this.handleSendClick();
         }
     };
 
+    /**
+     * This is called when a user hits "send" on the draw with GPT popup. It calls the drawWithGPT or regenerate
+     * functions depending on what mode is currently displayed, then sets various observable fields that facilitate
+     * what the user sees.
+     */
     @action
     handleSendClick = async () => {
         this._isLoading = true;
@@ -171,7 +215,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     };
 
     /**
-     * Calls GPT API to create a drawing based on user input
+     * Calls GPT API to create a drawing based on user input.
      */
     @action
     drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
@@ -182,6 +226,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             console.error('GPT call failed');
             return;
         }
+        console.log(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);
@@ -213,7 +258,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 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(this._selectedDoc);
+            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;
@@ -223,7 +268,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     };
 
     /**
-     * Parses the svg code that GPT returns into Bezier curves.
+     * Parses the svg code that GPT returns into Bezier curves, with coordinates and colors.
      */
     @action
     parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
@@ -307,26 +352,18 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         }}
                         icon={<FontAwesomeIcon icon="xmark" />}
                         color={SettingsManager.userColor}
-                        style={{ width: '19px' }}
                     />
                     <input
                         aria-label="Smart Draw Input"
                         className="smartdraw-input"
                         type="text"
                         autoFocus
-                        style={{ color: 'black' }}
                         value={this._userInput}
                         onChange={action(e => this._canInteract && (this._userInput = e.target.value))}
                         placeholder="Enter item to draw"
                         onKeyDown={this.handleKeyPress}
                     />
-                    <IconButton
-                        tooltip="Advanced Options"
-                        icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />}
-                        color={SettingsManager.userColor}
-                        style={{ width: '14px' }}
-                        onClick={action(() => (this._showOptions = !this._showOptions))}
-                    />
+                    <IconButton tooltip="Advanced Options" icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} color={SettingsManager.userColor} onClick={action(() => (this._showOptions = !this._showOptions))} />
                     <Button
                         style={{ alignSelf: 'flex-end' }}
                         text="Send"
@@ -337,8 +374,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     />
                 </div>
                 {this._showOptions && (
-                    <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
-                        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
+                    <div className="smartdraw-options">
+                        <div className="auto-color">
                             Auto color
                             <Switch
                                 sx={{
@@ -351,7 +388,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                                 onChange={action(e => this._canInteract && (this._autoColor = !this._autoColor))}
                             />
                         </div>
-                        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
+                        <div className="complexity">
                             Complexity
                             <Slider
                                 sx={{
@@ -369,15 +406,15 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                                 valueLabelDisplay="auto"
                             />
                         </div>
-                        <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
+                        <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` } },
                                 }}
-                                style={{ width: '80%' }}
                                 min={50}
                                 max={700}
                                 step={10}
@@ -403,7 +440,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     background: SettingsManager.userBackgroundColor,
                     color: SettingsManager.userColor,
                 }}>
-                <div style={{ display: 'flex', flexDirection: 'row' }}>
+                <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'} />}
@@ -412,12 +449,11 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     />
                     <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} />
                     {this._showEditBox && (
-                        <div style={{ display: 'flex', flexDirection: 'row' }}>
+                        <div className="edit-box">
                             <input
                                 aria-label="Edit instructions input"
                                 className="smartdraw-input"
                                 type="text"
-                                style={{ color: 'black' }}
                                 value={this._regenInput}
                                 onChange={action(e => this._canInteract && (this._regenInput = e.target.value))}
                                 onKeyDown={this.handleKeyPress}
-- 
cgit v1.2.3-70-g09d2


From 8c1f6f03de08e442df90dfbb6c9049e1c15ae353 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 12:27:20 -0400
Subject: don't unset activeTool in smartDraw handler and don't do anything if
 smartDraw is not shown

---
 src/client/views/smartdraw/SmartDrawHandler.tsx | 19 ++++++++++---------
 1 file changed, 10 insertions(+), 9 deletions(-)

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

diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index e362c0c89..d45baa7c5 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -148,15 +148,16 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     @action
     hideSmartDrawHandler = () => {
-        this.ShowRegenerate = false;
-        this._display = false;
-        this._isLoading = false;
-        this._showOptions = false;
-        this._userInput = '';
-        this._complexity = 5;
-        this._size = 350;
-        this._autoColor = true;
-        Doc.ActiveTool = InkTool.None;
+        if (this._display) {
+            this.ShowRegenerate = false;
+            this._display = false;
+            this._isLoading = false;
+            this._showOptions = false;
+            this._userInput = '';
+            this._complexity = 5;
+            this._size = 350;
+            this._autoColor = true;
+        }
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From a42cb938460f02aff22b0600ffde9dc70b6888ed Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 12:28:02 -0400
Subject: from last

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

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

diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index d45baa7c5..a77092a1a 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -157,6 +157,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             this._complexity = 5;
             this._size = 350;
             this._autoColor = true;
+            Doc.ActiveTool = InkTool.None;
         }
     };
 
-- 
cgit v1.2.3-70-g09d2


From b4d9626be5dd3fc158fd05589b5deca59a08dd4f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 14:39:42 -0400
Subject: cleaning up smart drawing menu options.

---
 src/client/views/InkTranscription.tsx              |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +-
 .../collections/collectionFreeForm/MarqueeView.tsx | 17 +++---
 src/client/views/global/globalScripts.ts           | 69 +---------------------
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  5 +-
 5 files changed, 14 insertions(+), 85 deletions(-)

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

diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index c6b55d55a..b4f4ebf63 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -344,7 +344,7 @@ export class InkTranscription extends React.Component {
         const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
 
         // calculate the necessary bounds from the selected ink docs
-        selected.map(
+        selected.forEach(
             action(d => {
                 const x = NumCast(d.x);
                 const y = NumCast(d.y);
@@ -367,7 +367,7 @@ export class InkTranscription extends React.Component {
         }
 
         // map through all the selected ink strokes and create the groupings
-        selected.map(
+        selected.forEach(
             action(d => {
                 const dx = NumCast(d.x);
                 const dy = NumCast(d.y);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index fe5ced29e..cdcc2328f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1246,9 +1246,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             );
             this._drawing.push(inkDoc);
         });
-        const collection = MarqueeView.getCollection(this._drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
-        console.log('is error here');
-        return collection;
+        return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 });
     };
 
     /**
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f1dba58ce..7614b5337 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -457,20 +457,19 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         let x_offset = 0;
         let y_offset = 0;
         let row_count = 0;
+        const newColDim = 900;
         for (const label of labelGroups) {
-            const newCollection = MarqueeView.getCollection([], undefined, false, { top: 1, left: 1, width: 1, height: 1 });
-            newCollection._width = 900;
-            newCollection._height = 900;
-            newCollection._x = this.Bounds.left;
-            newCollection._y = this.Bounds.top;
+            const newCollection = MarqueeView.getCollection([], undefined, false, this.Bounds);
+            newCollection._x = this.Bounds.left + x_offset;
+            newCollection._y = this.Bounds.top + y_offset;
+            newCollection._width = newColDim;
+            newCollection._height = newColDim;
             newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
             newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
-            newCollection._x = (newCollection._x as number) + x_offset;
-            newCollection._y = (newCollection._y as number) + y_offset;
-            x_offset += (newCollection._width as number) + 40;
+            x_offset += newColDim + 40;
             row_count += 1;
             if (row_count == 3) {
-                y_offset += (newCollection._height as number) + 40;
+                y_offset += newColDim + 40;
                 x_offset = 0;
                 row_count = 0;
             }
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index e9fcb1590..0ab431740 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,7 +1,6 @@
 /* eslint-disable @typescript-eslint/no-unused-vars */
 import { Colors } from 'browndash-components';
-import { action, runInAction } from 'mobx';
-import { aggregateBounds } from '../../../Utils';
+import { runInAction } from 'mobx';
 import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -384,72 +383,6 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
     return undefined;
 });
 
-export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) {
-    // TODO nda - if document being added to is a inkGrouping then we can just add to that group
-    if (Doc.ActiveTool === InkTool.Write) {
-        CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
-            // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
-            const selected = ffView.unprocessedDocs;
-            // loop through selected an get the bound
-            const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
-
-            selected.map(
-                action(d => {
-                    const x = NumCast(d.x);
-                    const y = NumCast(d.y);
-                    const width = NumCast(d._width);
-                    const height = NumCast(d._height);
-                    bounds.push({ x, y, width, height });
-                })
-            );
-
-            const aggregBounds = aggregateBounds(bounds, 0, 0);
-            const marqViewRef = ffView._marqueeViewRef.current;
-
-            // set the vals for bounds in marqueeView
-            if (marqViewRef) {
-                marqViewRef._downX = aggregBounds.x;
-                marqViewRef._downY = aggregBounds.y;
-                marqViewRef._lastX = aggregBounds.r;
-                marqViewRef._lastY = aggregBounds.b;
-            }
-
-            selected.map(
-                action(d => {
-                    const dx = NumCast(d.x);
-                    const dy = NumCast(d.y);
-                    delete d.x;
-                    delete d.y;
-                    delete d.activeFrame;
-                    delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-                    delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
-                    // calculate pos based on bounds
-                    if (marqViewRef?.Bounds) {
-                        d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2;
-                        d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2;
-                    }
-                    return d;
-                })
-            );
-            ffView._props.removeDocument?.(selected);
-            // TODO: nda - this is the code to actually get a new grouped collection
-            const newCollection = MarqueeView.getCollection(selected, undefined, true, { top: 1, left: 1, width: 1, height: 1 });
-            if (newCollection) {
-                newCollection.height = NumCast(newCollection._height);
-                newCollection.width = NumCast(newCollection._width);
-            }
-
-            // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
-            newCollection && ffView._props.addDocument?.(newCollection);
-            // TODO: nda - will probably need to go through and only remove the unprocessed selected docs
-            ffView.unprocessedDocs = [];
-
-            // InkTranscription.Instance.transcribeInk(newCollection, selected, false);
-        });
-    }
-    CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
-}
-
 function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
     InkTranscription.Instance?.createInkGroup();
     const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index a77092a1a..6d1277a08 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -228,9 +228,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             console.error('GPT call failed');
             return;
         }
-        console.log(res);
         const strokeData = await this.parseSvg(res, startPt, false, autoColor);
-        const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes);
+        const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
         drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
 
         this._errorOccurredOnce = false;
@@ -261,7 +260,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
             }
             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);
+            const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
             drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
             return strokeData;
         } catch (err) {
-- 
cgit v1.2.3-70-g09d2


From b56d2c2ae115e81fbf7aeaae0d2fed9ba073f11d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 15:19:52 -0400
Subject: more annopalette related cleanup

---
 src/client/util/DropConverter.ts                   | 39 ++++------------------
 src/client/views/DocumentDecorations.tsx           | 14 ++++----
 src/client/views/InkingStroke.tsx                  |  3 --
 .../views/collections/CollectionCarouselView.tsx   |  3 +-
 src/client/views/nodes/DocumentView.tsx            |  4 +--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  2 +-
 src/client/views/smartdraw/AnnotationPalette.tsx   |  4 +--
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 23 +++++++++----
 8 files changed, 37 insertions(+), 55 deletions(-)

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

diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 0ede44298..71cdaa58b 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -5,7 +5,7 @@ import { RichTextField } from '../../fields/RichTextField';
 import { ComputedField, ScriptField } from '../../fields/ScriptField';
 import { StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
-import { Docs } from '../documents/Documents';
+import { Docs, DocumentOptions } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { ButtonType, FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
 import { DragManager } from './DragManager';
@@ -64,49 +64,24 @@ export function MakeTemplate(doc: Doc) {
     return doc;
 }
 
-export function makeUserTemplateButton(doc: Doc) {
-    const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
-    if (layoutDoc.type !== DocumentType.FONTICON) {
-        !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
-    }
-    layoutDoc.isTemplateDoc = true;
-    const dbox = Docs.Create.FontIconDocument({
-        _nativeWidth: 100,
-        _nativeHeight: 100,
-        _width: 100,
-        _height: 100,
-        backgroundColor: StrCast(doc.backgroundColor),
-        title: StrCast(layoutDoc.title),
-        btnType: ButtonType.ClickButton,
-        icon: 'bolt',
-        isSystem: false,
-    });
-    dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
-    dbox.dragFactory = layoutDoc;
-    dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
-    dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)');
-    return dbox;
-}
-
 /**
- * Similar to makeUserTemplateButton, but rather than creating a draggable button for the template, it takes in
- * an ImageField that will display.
+ * Makes a draggable button or image that will create a template doc Instance
  */
-export function makeUserTemplateImage(doc: Doc, imageHref: string | undefined) {
-    const image = imageHref ?? 'http://www.cs.brown.edu/~bcz/noImage.png';
+export function makeUserTemplateButtonOrImage(doc: Doc, image: string | undefined) {
     const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
     if (layoutDoc.type !== DocumentType.FONTICON) {
         !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
     }
     layoutDoc.isTemplateDoc = true;
-    const dbox = Docs.Create.ImageDocument(image, {
+    const docOptions: DocumentOptions = {
         _nativeWidth: 100,
         _nativeHeight: 100,
         _width: 100,
         _height: 100,
         title: StrCast(layoutDoc.title),
         isSystem: false,
-    });
+    };
+    const dbox = image ? Docs.Create.ImageDocument(image, docOptions) : Docs.Create.FontIconDocument({ ...docOptions, backgroundColor: StrCast(doc.backgroundColor), btnType: ButtonType.ClickButton, icon: 'bolt' });
     dbox.title = ComputedField.MakeFunction('this.dragFactory.title');
     dbox.dragFactory = layoutDoc;
     dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
@@ -129,7 +104,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
                 });
             }
         } else if (!doc.onDragStart && !doc.isButtonBar) {
-            dbox = makeUserTemplateButton(doc);
+            dbox = makeUserTemplateButtonOrImage(doc);
         } else if (doc.isButtonBar) {
             dbox.ignoreClick = true;
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index df9ec1bbf..e569c7dc8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -231,15 +231,17 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
                     views.forEach(iconView => {
                         const iconViewDoc = iconView.Document;
                         Doc.setNativeView(iconViewDoc);
+                        // bcz: hacky ... when closing a Doc do different things depending on the contet ...
                         if (iconViewDoc.activeFrame) {
-                            iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation.
+                            iconViewDoc.opacity = 0; // if in an animation collection, set opacity to 0 to allow inkMasks and other documents to remain in the collection and to smoothly animate when they are activated in a different animation frame
                         } else {
-                            // to mark annotations as no longer saved if they're deleted from the palette
-                            const dragFactory: Doc = DocCast(iconView.Document.dragFactory);
-                            if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) {
-                                DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
-                            }
+                            // if Doc is in the annotation palette, remove the flag indicating that it's saved
+                            const dragFactory = DocCast(iconView.Document.dragFactory);
+                            if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
+
+                            // if this is a face Annotation doc, then just hide it.
                             if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
+                            // otherwise actually remove the Doc from its parent collection
                             else iconView._props.removeDocument?.(iconView.Document);
                         }
                     });
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index bc2b4badb..f44c1720d 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -30,7 +30,6 @@ import { InkData, InkField } from '../../fields/InkField';
 import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
 import { Gestures } from '../../pen-gestures/GestureTypes';
-import { CognitiveServices } from '../cognitive_services/CognitiveServices';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { InteractionUtils } from '../util/InteractionUtils';
@@ -122,8 +121,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
             InkTranscription.Instance.transcribeInk(newCollection, selected, false);
         }
-        // const data: InkData = this.inkScaledData().inkData ?? [];
-        // CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
     };
 
     /**
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 143cfe0e8..db75c9e8b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -144,7 +144,6 @@ export class CollectionCarouselView extends CollectionSubView() {
         revealItems.push({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;},     icon: 'pencil',}); // prettier-ignore
         !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
     };
-    childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null));
 
     isChildContentActive = () =>
         this._props.isContentActive?.() === false
@@ -165,7 +164,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                 Document={doc}
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
-                fitWidth={undefined}
+                fitWidth={this._props.childLayoutFitWidth}
                 showTags={true}
                 containerViewPath={this.childContainerViewPath}
                 setContentViewBox={undefined}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 85fd42ddf..80b61b6a9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -27,7 +27,7 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
+import { MakeTemplate, makeUserTemplateButtonOrImage } from '../../util/DropConverter';
 import { UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
@@ -1348,7 +1348,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
                     tempDoc = view.Document;
                     MakeTemplate(tempDoc);
                     Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
-                    Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc));
+                    Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc));
                     tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc);
                 } else {
                     tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 84d3fc748..d1b0dc7bf 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -268,7 +268,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const container = DocCast(this.Document.embedContainer);
             const docView = DocumentView.getDocumentView?.(container);
             docView?.ComponentView?._props.addDocument?.(drawing);
-            drawing.x = NumCast(this.Document.x) + (this.Document.width as number);
+            drawing.x = NumCast(this.Document.x) + NumCast(this.Document.width);
             drawing.y = NumCast(this.Document.y);
         };
 
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 0c8dbf12d..4421547be 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -13,7 +13,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { ImageCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { makeUserTemplateImage } from '../../util/DropConverter';
+import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
@@ -123,7 +123,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
             const { clone } = await Doc.MakeClone(doc);
             clone.title = doc.title;
             const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
-            Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateImage(clone, image));
+            Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image));
             doc.savedAsAnno = true;
         }
     };
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 6d1277a08..76bfe2a19 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Slider, Switch } from '@mui/material';
 import { Button, IconButton } from 'browndash-components';
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import React from 'react';
 import { AiOutlineSend } from 'react-icons/ai';
@@ -194,14 +194,21 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._canInteract = false;
         if (this.ShowRegenerate) {
             await this.regenerate();
-            this._regenInput = '';
-            this._showEditBox = false;
+            runInAction(() => {
+                this._regenInput = '';
+                this._showEditBox = false;
+            });
         } else {
-            this._showOptions = false;
+            runInAction(() => {
+                this._showOptions = false;
+            });
             try {
                 await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
                 this.hideSmartDrawHandler();
-                this.ShowRegenerate = true;
+
+                runInAction(() => {
+                    this.ShowRegenerate = true;
+                });
             } catch (err) {
                 if (this._errorOccurredOnce) {
                     console.error('GPT call failed', err);
@@ -212,8 +219,10 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 }
             }
         }
-        this._isLoading = false;
-        this._canInteract = true;
+        runInAction(() => {
+            this._isLoading = false;
+            this._canInteract = true;
+        });
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 8d7bf0588ca63c2d505a494865dcf9921eb1383d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Oct 2024 15:57:57 -0400
Subject: more lint fixes

---
 src/client/util/CurrentUserUtils.ts                   |  2 +-
 src/client/util/DropConverter.ts                      |  2 +-
 src/client/util/Scripting.ts                          |  5 -----
 src/client/views/DocumentDecorations.tsx              |  2 +-
 src/client/views/InkTranscription.tsx                 |  1 +
 src/client/views/InkingStroke.tsx                     |  4 +---
 src/client/views/LightboxView.tsx                     |  2 --
 src/client/views/Main.tsx                             |  1 -
 src/client/views/MainView.tsx                         | 19 +++++++------------
 .../views/collections/CollectionCarousel3DView.tsx    |  2 +-
 .../views/collections/CollectionCarouselView.tsx      |  4 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx     | 11 ++++++-----
 .../collections/collectionFreeForm/MarqueeView.tsx    |  3 +--
 src/client/views/nodes/FontIconBox/FontIconBox.tsx    |  1 -
 src/client/views/nodes/ImageBox.tsx                   |  1 -
 src/client/views/nodes/PDFBox.tsx                     |  5 +----
 .../views/nodes/formattedText/FormattedTextBox.tsx    |  8 --------
 src/client/views/nodes/formattedText/RichTextMenu.tsx |  1 -
 src/client/views/pdf/PDFViewer.tsx                    |  2 --
 src/client/views/smartdraw/AnnotationPalette.tsx      |  4 +---
 src/client/views/smartdraw/SmartDrawHandler.tsx       |  3 ++-
 src/fields/SchemaHeaderField.ts                       |  1 -
 22 files changed, 26 insertions(+), 58 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 59760ba32..96d69e7a1 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,7 +2,7 @@
 import { reaction, runInAction } from "mobx";
 import * as rp from 'request-promise';
 import { ClientUtils, OmitKeys } from "../../ClientUtils";
-import { Doc, DocListCast, DocListCastAsync, FieldType, Opt, StrListCast } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
 import { DocData } from "../../fields/DocSymbols";
 import { InkTool } from "../../fields/InkField";
 import { List } from "../../fields/List";
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index 71cdaa58b..b5d29be4c 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -67,7 +67,7 @@ export function MakeTemplate(doc: Doc) {
 /**
  * Makes a draggable button or image that will create a template doc Instance
  */
-export function makeUserTemplateButtonOrImage(doc: Doc, image: string | undefined) {
+export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) {
     const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
     if (layoutDoc.type !== DocumentType.FONTICON) {
         !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index c63d3d7cb..c7b86815a 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -60,7 +60,6 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
     // let params: any[] = [Docs, ...fieldTypes];
     const compiledFunction = (() => {
         try {
-            // eslint-disable-next-line no-new-func
             return new Function(...paramNames, `return ${script}`);
         } catch (e) {
             console.log(e);
@@ -69,10 +68,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
     })();
     if (!compiledFunction) return { compiled: false, errors };
     const { capturedVariables = {} } = options;
-    // eslint-disable-next-line default-param-last
     const run = (args: { [name: string]: unknown } = {}, onError?: (e: string) => void, errorVal?: ts.Diagnostic): ScriptResult => {
         const argsArray: unknown[] = [];
-        // eslint-disable-next-line no-restricted-syntax
         for (const name of customParams) {
             if (name !== 'this') {
                 argsArray.push(name in args ? args[name] : capturedVariables[name]);
@@ -224,7 +221,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
     if ('this' in params || 'this' in capturedVariables) {
         paramNames.push('this');
     }
-    // eslint-disable-next-line no-restricted-syntax
     for (const key in params) {
         if (key !== 'this') {
             paramNames.push(key);
@@ -234,7 +230,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
         const val = params[key];
         return `${key}: ${val}`;
     });
-    // eslint-disable-next-line no-restricted-syntax
     for (const key in capturedVariables) {
         if (key !== 'this') {
             const val = capturedVariables[key];
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index e569c7dc8..1c0d51e17 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import { IconButton } from 'browndash-components';
-import { action, computed, makeObservable, observable, runInAction, trace } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { FaUndo } from 'react-icons/fa';
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index b4f4ebf63..24d53a8c8 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -18,6 +18,7 @@ import { URLField } from '../../fields/URLField';
  * Class component that handles inking in writing mode
  */
 export class InkTranscription extends React.Component {
+    // eslint-disable-next-line no-use-before-define
     static Instance: InkTranscription;
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f44c1720d..270266a94 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -51,7 +51,7 @@ import { InkTranscription } from './InkTranscription';
 import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { DocumentView } from './nodes/DocumentView';
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
 @observer
@@ -473,7 +473,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                         // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
                         cursor: this._props.isSelected() ? 'default' : undefined,
                     }}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...interactions}>
                     {clickableLine(this.onPointerDown, isInkMask)}
                     {isInkMask ? null : inkLine}
@@ -490,7 +489,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             // top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2,
                         }}>
                         <FormattedTextBox
-                            // eslint-disable-next-line react/jsx-props-no-spreading
                             {...this._props}
                             setHeight={undefined}
                             setContentViewBox={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 331b3db64..a543b4875 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -46,7 +46,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
         ); 
     } // prettier-ignore
     public static LightboxDoc = () => LightboxView.Instance?._doc;
-    // eslint-disable-next-line no-use-before-define
     static Instance: LightboxView;
     private _path: {
         doc: Opt<Doc>; //
@@ -341,7 +340,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
 }
 interface LightboxTourBtnProps {
     navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: IconProp, display: boolean, click: () => void, color?: string) => JSX.Element;
-    // eslint-disable-next-line react/no-unused-prop-types
     future: () => Opt<Doc[]>;
     stepInto: () => void;
     lightboxDoc: () => Opt<Doc>;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 94294e97a..73d2872d1 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable no-new */
 // if ((module as any).hot) {
 //     (module as any).hot.accept();
 // }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8e921ca5e..fa2e94d12 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -3,7 +3,7 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
 import * as far from '@fortawesome/free-regular-svg-icons';
 import * as fa from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ResizeObserver from 'resize-observer-polyfill';
@@ -77,7 +77,7 @@ import { TopBar } from './topbar/TopBar';
 import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
 import { InkTranscription } from './InkTranscription';
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
 
 @observer
@@ -977,13 +977,11 @@ export class MainView extends ObservableReactComponent<object> {
             <div className="mainView-snapLines">
                 <svg style={{ width: '100%', height: '100%' }}>
                     {[
-                        ...SnappingManager.HorizSnapLines.map((l, i) => (
-                            // eslint-disable-next-line react/no-array-index-key
-                            <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        ...SnappingManager.HorizSnapLines.map(l => (
+                            <line key={'horiz' + l} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
                         )),
-                        ...SnappingManager.VertSnapLines.map((l, i) => (
-                            // eslint-disable-next-line react/no-array-index-key
-                            <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+                        ...SnappingManager.VertSnapLines.map(l => (
+                            <line key={'vert' + l} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
                         )),
                     ]}
                 </svg>
@@ -1071,10 +1069,7 @@ export class MainView extends ObservableReactComponent<object> {
                         docView={DocButtonState.Instance.LinkEditorDocView}
                     />
                 ) : null}
-                {LinkInfo.Instance?.LinkInfo ? (
-                    // eslint-disable-next-line react/jsx-props-no-spreading
-                    <LinkDocPreview {...LinkInfo.Instance.LinkInfo} />
-                ) : null}
+                {LinkInfo.Instance?.LinkInfo ? <LinkDocPreview {...LinkInfo.Instance.LinkInfo} /> : null}
                 {((page: string) => {
                     // prettier-ignore
                     switch (page) {
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index cf86a0a4e..c5da8e037 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -16,6 +16,7 @@ import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { Transform } from '../../util/Transform';
 
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
 
 @observer
@@ -88,7 +89,6 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
         const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => (
             <DocumentView
-                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
                 Document={childPair.layout}
                 TemplateDataDocument={childPair.data}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index db75c9e8b..74cf580c9 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -3,8 +3,8 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { Doc, Opt } from '../../../fields/Doc';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { ContextMenu } from '../ContextMenu';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cdcc2328f..0cc63d632 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -510,12 +510,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         e.stopPropagation();
                         break;
                     case InkTool.SmartDraw:
-                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, e => this.showSmartDraw(e.pageX, e.pageY), hit !== -1);
+                        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, () => this.showSmartDraw(e.pageX, e.pageY), hit !== -1);
                         e.stopPropagation();
+                        break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-                            const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
+                            const ahit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, ahit !== -1, false);
                         }
                         break;
                     default:
@@ -655,13 +656,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
+    onEraserClick = (e: PointerEvent) => {
         e.preventDefault();
         e.stopImmediatePropagation();
         this.erase(e, [0, 0]);
     };
 
-    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
+    forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: string) => {
         this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
     };
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 7614b5337..b1f6815b3 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -57,6 +57,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
     }
 
+    // eslint-disable-next-line no-use-before-define
     static Instance: MarqueeView;
 
     constructor(props: SubCollectionViewProps & MarqueeViewProps) {
@@ -548,7 +549,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
     };
 
     touchesLine(r1: { left: number; top: number; width: number; height: number }) {
-        // eslint-disable-next-line no-restricted-syntax
         for (const lassoPt of this._lassoPts) {
             const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
             if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) {
@@ -569,7 +569,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             let hasLeft = false;
             let hasBottom = false;
             let hasRight = false;
-            // eslint-disable-next-line no-restricted-syntax
             for (const lassoPt of this._lassoPts) {
                 const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
                 hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height);
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index b7e1350ca..feaf84b7b 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index aa4376bb2..ec5e062c8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -493,7 +493,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }}>
                 <CollectionFreeFormView
                     ref={this._ffref}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     setContentViewBox={emptyFunction}
                     NativeWidth={returnZero}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 42ac51107..7ef431885 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types';
+import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction } from '../../../Utils';
@@ -538,7 +538,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     Document={this.Document}
                     layoutDoc={this.layoutDoc}
@@ -554,7 +553,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this._props.select(false), true)}>
                     <ComponentTag
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         setContentViewBox={emptyFunction} // override setContentView to do nothing
                         NativeWidth={this.sidebarNativeWidthFunc}
@@ -608,7 +606,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         top: 0,
                     }}>
                     <PDFViewer
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         pdfBox={this}
                         sidebarAddDoc={this.sidebarAddDocument}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d1b0dc7bf..d3bc08bd3 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -655,7 +655,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                         textContent += node.child(i).textContent;
                         i++;
                     }
-                    // eslint-disable-next-line no-cond-assign
                     while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) {
                         const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1));
                         ret.push(sel);
@@ -714,7 +713,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const hr = Math.round(Date.now() / 1000 / 60 / 60);
             numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
         }
-        // eslint-disable-next-line operator-assignment
         this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx.  so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView)
     };
 
@@ -1131,7 +1129,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             const nodes: Node[] = [];
             let hadStart = start !== 0;
             frag.forEach((node, index) => {
-                // eslint-disable-next-line no-use-before-define
                 const examinedNode = findAnchorNode(node, editor);
                 if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
                     nodes.push(examinedNode.node);
@@ -1285,7 +1282,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             action(selected => {
                 this.prepareForTyping();
                 if (FormattedTextBox._globalHighlights.has('Bold Text')) {
-                    // eslint-disable-next-line operator-assignment
                     this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
                 }
                 if (RichTextMenu.Instance?.view === this._editorView && !selected) {
@@ -1711,7 +1707,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/);
         if (match) {
             this.dataDoc.title_custom = true;
-            // eslint-disable-next-line prefer-destructuring
             this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc
             this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1));
         }
@@ -1793,7 +1788,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
         const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
         if (children && !SnappingManager.IsDragging) {
-            // eslint-disable-next-line no-use-before-define
             const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
             const toNum = (val: string) => Number(val.replace('px', ''));
             const toHgt = (node: Element): number => {
@@ -1880,7 +1874,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             return ComponentTag === CollectionStackingView ? (
                 <SidebarAnnos
                     ref={this._sidebarRef}
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     {...this._props}
                     Document={this.Document}
                     layoutDoc={this.layoutDoc}
@@ -1900,7 +1893,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
                     <ComponentTag
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         ref={this._sidebarTagRef}
                         setContentView={emptyFunction}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 1560d51a1..88e2e4248 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -697,7 +697,6 @@ interface RichTextMenuPluginProps {
     editorProps: FormattedTextBoxProps;
 }
 export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
-    // eslint-disable-next-line react/no-unused-class-component-methods
     update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) {
         RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc);
     }
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 01242ba48..7a86ee802 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -504,7 +504,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
         return (
             <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this._props.Document), transform: `scale(${NumCast(this._props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}>
                 {inlineAnnos.map(anno => (
-                    // eslint-disable-next-line react/jsx-props-no-spreading
                     <Annotation {...this._props} fieldKey={this._props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} containerDataDoc={this._props.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
                 ))}
             </div>
@@ -537,7 +536,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
                 pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
             }}>
             <CollectionFreeFormView
-                // eslint-disable-next-line react/jsx-props-no-spreading
                 {...this._props}
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
index 1c17469c3..f1e2e4f41 100644
--- a/src/client/views/smartdraw/AnnotationPalette.tsx
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -11,6 +11,7 @@ import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { ImageCast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
@@ -23,8 +24,6 @@ import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
 import { FieldView } from '../nodes/FieldView';
 import './AnnotationPalette.scss';
 import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
-import { ImageField } from '../../../fields/URLField';
-import { Copy } from '../../../fields/FieldSymbols';
 
 interface AnnotationPaletteProps {
     Document: Doc;
@@ -158,7 +157,6 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett
                 console.log('Error generating drawing', e);
             }
         }
-        // eslint-disable-next-line @typescript-eslint/no-unused-expressions
         this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
         this._userInput = '';
         this._isLoading = false;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 76bfe2a19..75ef55060 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -50,6 +50,7 @@ export interface DrawingOptions {
 
 @observer
 export class SmartDrawHandler extends ObservableReactComponent<object> {
+    // eslint-disable-next-line no-use-before-define
     static Instance: SmartDrawHandler;
 
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
@@ -395,7 +396,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                                 defaultChecked={true}
                                 value={this._autoColor}
                                 size="small"
-                                onChange={action(e => this._canInteract && (this._autoColor = !this._autoColor))}
+                                onChange={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
                             />
                         </div>
                         <div className="complexity">
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 0a8dd1d9e..5f4d59cf9 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -79,7 +79,6 @@ export class SchemaHeaderField extends ObjectField {
     @serializable(primitive())
     desc: boolean | undefined; // boolean determines sort order, undefined when no sort
 
-    // eslint-disable-next-line default-param-last
     constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
         super();
 
-- 
cgit v1.2.3-70-g09d2

