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/collections/collectionFreeForm/MarqueeOptionsMenu.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index f02cd9d45..b3fdd9379 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,6 +3,7 @@ import { IconButton } from 'browndash-components';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
+import { Doc } from '../../../../fields/Doc';
 import { unimplementedFunction } from '../../../../Utils';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
@@ -12,7 +13,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
     // eslint-disable-next-line no-use-before-define
     static Instance: MarqueeOptionsMenu;
 
-    public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction;
+    public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction;
     public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
     public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
     public showMarquee: () => void = unimplementedFunction;
-- 
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/collections/collectionFreeForm/MarqueeOptionsMenu.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/collections/collectionFreeForm/MarqueeOptionsMenu.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

