From 01db98ebdd02729729222bdd20ab65b57cbbe94c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 9 Oct 2024 13:27:56 -0400
Subject: more refactoring to of collection flashcards into CollectioSubView to
 simplify using it in diferent collection views.

---
 src/client/views/collections/CollectionSubView.tsx | 51 ++++++++++++++++++++--
 1 file changed, 48 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 581201a20..c057d2402 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -25,7 +25,8 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldViewProps } from '../nodes/FieldView';
-import { DocumentView } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -119,7 +120,8 @@ export function CollectionSubView<X>() {
                     pair =>
                         // filter out any documents that have a proto that we don't have permissions to
                         !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate))
-                );
+                )
+                .filter(pair => !this._filterFunc?.(pair.layout!));
             return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
         }
         /**
@@ -515,6 +517,49 @@ export function CollectionSubView<X>() {
                 alert('Document upload failed - possibly an unsupported file type.');
             }
         };
+
+        protected _sideBtnWidth = 35;
+        @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
+        /**
+         * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
+         */
+        @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+        /**
+         * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection
+         * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
+         * size or a fraction of the collection view.
+         */
+        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+        /**
+         * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
+         * Note, the scale factor does not allow for elements to grow larger than their native screen space size.
+         */
+        @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore
+
+        screenXPadding = () => (this.uiBtnScaling * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale;
+        filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
+        childDocsFunc = () => this.childDocs;
+        @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore
+
+        public flashCardUI = (curDoc: () => Doc | undefined, docViewProps: () => DocumentViewProps, answered?: (correct: boolean) => void) => {
+            return (
+                <FlashcardPracticeUI
+                    setFilterFunc={this.setFilterFunc}
+                    fieldKey={this.fieldKey}
+                    sideBtnWidth={this._sideBtnWidth}
+                    allChildDocs={this.childDocsFunc}
+                    filteredChildDocs={this.filteredChildDocs}
+                    advance={answered}
+                    curDoc={curDoc}
+                    practiceBtnOffset={this._sideBtnWidth * 4}
+                    layoutDoc={this.layoutDoc}
+                    uiBtnScaling={this.uiBtnScaling}
+                    ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
+                    renderDepth={this._props.renderDepth}
+                    docViewProps={docViewProps}
+                />
+            );
+        };
     }
 
     return CollectionSubViewInternal;
-- 
cgit v1.2.3-70-g09d2


From 386d640fe7fc4b443bc5f241f86e27424851dc4e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Oct 2024 16:24:00 -0400
Subject: adjusted placement of flaschard practice buttons to be closer to the
 bottom.  Fixed being able to enter quiz mode for flaschards created as part
 of a stack by fixing embedContainer setting.

---
 src/client/documents/Documents.ts                     | 15 ++++++---------
 src/client/views/collections/CollectionSubView.tsx    |  1 -
 src/client/views/collections/FlashcardPracticeUI.scss | 16 +++++++++-------
 src/client/views/collections/FlashcardPracticeUI.tsx  |  3 +--
 4 files changed, 16 insertions(+), 19 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 358bc227e..d529de8e5 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -723,6 +723,9 @@ export namespace Docs {
                 updateCachedAcls(dataDoc);
                 updateCachedAcls(viewDoc);
 
+                if (data instanceof List) {
+                    data.map(item => item instanceof Doc && Doc.SetContainer(item, viewDoc));
+                }
                 return viewDoc;
             }
 
@@ -907,15 +910,13 @@ export namespace Docs {
         }
 
         export function CalendarDocument(options: DocumentOptions, documents: Array<Doc>) {
-            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), {
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), {
                 _layout_nativeDimEditable: true,
                 _layout_reflowHorizontal: true,
                 _layout_reflowVertical: true,
                 ...options,
                 _type_collection: CollectionViewType.Calendar,
             });
-            documents.forEach(d => Doc.SetContainer(d, inst));
-            return inst;
         }
 
         // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
@@ -924,9 +925,7 @@ export namespace Docs {
         // }
 
         export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id);
-            documents.forEach(d => Doc.SetContainer(d, inst));
-            return inst;
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id);
         }
 
         export function ConfigDocument(options: DocumentOptions, id?: string) {
@@ -1031,9 +1030,7 @@ export namespace Docs {
         }
 
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
-            const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeView_FreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
-            documents.map(c => Doc.SetContainer(c, ret));
-            return ret;
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeView_FreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
         }
 
         export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index c057d2402..f85b0b433 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -551,7 +551,6 @@ export function CollectionSubView<X>() {
                     filteredChildDocs={this.filteredChildDocs}
                     advance={answered}
                     curDoc={curDoc}
-                    practiceBtnOffset={this._sideBtnWidth * 4}
                     layoutDoc={this.layoutDoc}
                     uiBtnScaling={this.uiBtnScaling}
                     ScreenToLocalBoxXf={this.ScreenToLocalBoxXf}
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 2f99500f8..4ed27793d 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -16,13 +16,15 @@
 .FlashcardPracticeUI-practice {
     position: absolute;
     width: 100%;
-    pointer-events: all;
-}
-.FlashcardPracticeUI-remove {
-    left: 52%;
-}
-.FlashcardPracticeUI-check {
-    right: 52%;
+    pointer-events: none;
+    .FlashcardPracticeUI-remove {
+        left: 52%;
+        pointer-events: all;
+    }
+    .FlashcardPracticeUI-check {
+        right: 52%;
+        pointer-events: all;
+    }
 }
 .FlashcardPracticeUI-menu {
     position: absolute;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 0e9fe89c9..4e424f5cd 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -37,7 +37,6 @@ interface PracticeUIProps {
     ScreenToLocalBoxXf: () => Transform;
     docViewProps: () => DocumentViewProps;
     setFilterFunc: (func?: (doc: Doc) => boolean) => void;
-    practiceBtnOffset?: number;
 }
 @observer
 export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> {
@@ -105,7 +104,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         };
 
         return this.practiceMode == practiceMode.PRACTICE && this._props.curDoc() ? (
-            <div className="FlashcardPracticeUI-practice" style={{ transform: `scale(${this._props.uiBtnScaling})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px` }}>
+            <div className="FlashcardPracticeUI-practice" style={{ transform: `scale(${this._props.uiBtnScaling})`, bottom: `${this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px` }}>
                 <Tooltip title="Incorrect. View again later.">
                     <div key="remove" className="FlashcardPracticeUI-remove" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => setPracticeVal(e, practiceVal.MISSED))}>
                         <FontAwesomeIcon icon="xmark" color="red" size="1x" />
-- 
cgit v1.2.3-70-g09d2


From 29b83f023442c313ca5cf95f70f6430f101060e6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 14 Oct 2024 19:55:32 -0400
Subject: reorganized comparisonBox related components -- moved stuff down into
 Docs.Crete and CurrentUserUtils.  changed Doc.Copy to copy Doc's in fields
 tagged with cloneOnCopy.  Changed ComparisonBox to support hover for slide or
 flip views. Fixed pointerEfvents for hover in comparisonBox

---
 src/client/documents/DocUtils.ts                   |   2 -
 src/client/documents/Documents.ts                  |  43 ++++-
 src/client/util/CurrentUserUtils.ts                |   4 +-
 .../views/collections/CollectionCarousel3DView.tsx |  33 ++--
 .../views/collections/CollectionCarouselView.tsx   |  58 +++---
 src/client/views/collections/CollectionSubView.tsx |   2 +-
 src/client/views/collections/CollectionView.tsx    |   6 +-
 .../views/collections/FlashcardPracticeUI.tsx      |  36 ++--
 src/client/views/nodes/ComparisonBox.scss          |   3 +-
 src/client/views/nodes/ComparisonBox.tsx           | 196 +++++++++++----------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  20 ---
 src/fields/Doc.ts                                  |   2 +-
 12 files changed, 223 insertions(+), 182 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 5f54f9d0a..19f3c89ef 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -103,7 +103,6 @@ export namespace DocUtils {
                       return false;
                   }
                   const facetKeys = Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== ClientUtils.noDragDocsFilter.split(Doc.FilterSep)[0]);
-                  // eslint-disable-next-line no-restricted-syntax
                   for (const facetKey of facetKeys) {
                       const facet = filterFacets[facetKey];
 
@@ -288,7 +287,6 @@ export namespace DocUtils {
         return doc;
     }
     export function AssignDocField(doc: Doc, field: string, creator: (reqdOpts: DocumentOptions, items?: Doc[]) => Doc, reqdOpts: DocumentOptions, items?: Doc[], scripts?: { [key: string]: string }, funcs?: { [key: string]: string }) {
-        // eslint-disable-next-line no-return-assign
         return DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts, items) ?? (doc[field] = creator(reqdOpts, items)), scripts, funcs);
     }
 
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f71b9f879..99af1f1a9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -18,6 +18,7 @@ import { PointData } from '../../pen-gestures/GestureTypes';
 import { DocServer } from '../DocServer';
 import { dropActionType } from '../util/DropActionTypes';
 import { CollectionViewType, DocumentType } from './DocumentTypes';
+import { Id } from '../../fields/FieldSymbols';
 
 class EmptyBox {
     public static LayoutString() {
@@ -360,6 +361,7 @@ export class DocumentOptions {
     isFolder?: BOOLt = new BoolInfo('is document a tree view folder');
     _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label');
     _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target');
+    cloneOnCopy?: BOOLt = new BoolInfo('if this Doc is a field of another Doc, then it should be copied when the other Doc is copied');
 
     mapPin?: DOCt = new DocInfo('pin associated with a config anchor', false);
     config_latitude?: NUMt = new NumInfo('latitude of a map', false);
@@ -420,6 +422,12 @@ export class DocumentOptions {
     flexGap?: NUMt = new NumInfo('Linear view flex gap');
     flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse';
 
+    // Comparison
+    data_revealOp?: STRt = new StrInfo("visual reveal type for front and back of comparison - 'slide' or 'flip' ");
+    data_revealOp_hover?: BOOLt = new BoolInfo('reveal back of comparison manually or by hovering');
+    data_front?: DOCt = new DocInfo('contents of front of flashcard/comparison');
+    data_back?: DOCt = new DocInfo('contents of back of flashcard/comparison');
+
     link?: string;
     link_description?: string; // added for links
     link_relationship?: string; // type of relatinoship a link represents
@@ -784,8 +792,39 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), '', options);
         }
 
-        export function ComparisonDocument(text: string, options: DocumentOptions = { title: 'Comparison Box' }) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), text, options);
+        export function ComparisonDocument(title: string, options: DocumentOptions) {
+            return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options);
+        }
+        /**
+         * Creates a text box where the supplied text (and optional iimage) will be vertically
+         * and horizontally centered.  If text_placeholder is set to true, then the text will be
+         * treated as placeholder text and automatically selected when the text box is selected.
+         * @param title name of text box
+         * @param text text to display in text box
+         * @param opts metadata fields to set on text box
+         * @param img optional image to add to text box
+         * @returns
+         */
+        export function CenteredTextCreator(title: string, text: string, opts: DocumentOptions, img?: Doc) {
+            return TextDocument(RichTextField.textToRtf(text, img?.[Id]), {
+                title, //
+                _layout_autoHeight: true,
+                _layout_centered: true,
+                text_align: 'center',
+                _layout_fitWidth: true,
+                ...opts,
+            });
+        }
+
+        export function FlashcardDocument(title: string, front?: Doc, back?: Doc, options: DocumentOptions = { title: 'Flashcard' }) {
+            return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', {
+                data_front: front ?? CenteredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', { text_placeholder: true, cloneOnCopy: true }, undefined),
+                data_back: back ?? CenteredTextCreator('answer', 'answer here', { text_placeholder: true, cloneOnCopy: true }, undefined),
+                _layout_fitWidth: true,
+                _layout_isFlashcard: true,
+                title,
+                ...options,
+            });
         }
         export function DiagramDocument(options: DocumentOptions = { title: '' }) {
             return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options);
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 798cdf5a9..555ccdd88 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -369,14 +369,14 @@ pie title Minerals in my tap water
                           creator:(opts:DocumentOptions)=> Doc // how to create the empty thing if it doesn't exist
               }[] = [
             {key: "Note",       creator: opts => Docs.Create.TextDocument("", opts),        opts: { _width: 200, _layout_autoHeight: true }},
-            {key: "Flashcard",  creator: opts => Docs.Create.ComparisonDocument("", opts),  opts: { _layout_isFlashcard: true, _layout_fitWidth: true, _width: 300, _height: 300}},
+            {key: "Flashcard",  creator: opts => Docs.Create.FlashcardDocument("", undefined, undefined, opts),opts: { _width: 300, _height: 300}},
             {key: "Image",      creator: opts => Docs.Create.ImageDocument("", opts),       opts: { _width: 400, _height:400 }},
             {key: "Equation",   creator: opts => Docs.Create.EquationDocument("",opts),     opts: { _width: 300, _height: 35, }},
             {key: "Noteboard",  creator: opts => Docs.Create.NoteTakingDocument([], opts),  opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
             {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts),      opts: { _width: 300, _height: 300, }},
             {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts),    opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
             {key: "Webpage",    creator: opts => Docs.Create.WebDocument("",opts),          opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }},
-            {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("",opts),   opts: { _width: 300, _height: 300 }},
+            {key: "Comparison", creator: opts => Docs.Create.ComparisonDocument("", opts),  opts: { _width: 300, _height: 300 }},
             {key: "Diagram",    creator: Docs.Create.DiagramDocument,                       opts: { _width: 300, _height: 300, _type_collection: CollectionViewType.Freeform, layout_diagramEditor: CollectionView.LayoutString("data") }, scripts: { onPaint: `toggleDetail(documentView, "diagramEditor","")`}},
             {key: "Audio",      creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
             {key: "Audio",      creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, _layout_fitWidth: true, }},
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 05be376ca..e9ace733e 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -15,6 +15,7 @@ import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { computedFn } from 'mobx-utils';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -53,18 +54,22 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     centerScale = Number(CAROUSEL3D_CENTER_SCALE);
     sideScale = Number(CAROUSEL3D_SIDE_SCALE);
-    panelWidth = () => this._props.PanelWidth() / 3;
-    panelHeight = () => this._props.PanelHeight() * this.sideScale;
+    panelWidth = () => this._props.PanelWidth() / 3 / this.nativeScaling();
+    panelHeight = () => (this._props.PanelHeight() * this.sideScale) / this.nativeScaling();
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
-    isChildContentActive = () =>
-        this._props.isContentActive?.() === false
-            ? false
-            : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
-              ? true
-              : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+    isChildContentActive = computedFn(
+        (doc: Doc) => () =>
+            this._props.isContentActive?.() === false
                 ? false
-                : undefined;
+                : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+                  ? true
+                  : this._props.isContentActive?.() && this.curDoc() === doc
+                    ? true
+                    : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+                      ? false
+                      : undefined
+    );
     contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     childScreenLeftToLocal = () =>
         this.contentScreenToLocalXf()
@@ -110,7 +115,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 LayoutTemplateString={this._props.childLayoutString}
                 focus={this.focus}
                 ScreenToLocalTransform={dxf}
-                isContentActive={this.isChildContentActive}
+                isContentActive={this.isChildContentActive(child)}
                 isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
                 PanelWidth={this.panelWidth}
                 PanelHeight={this.panelHeight}
@@ -125,7 +130,6 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     }
 
     changeSlide = (direction: number) => {
-        DocumentView.DeselectAll();
         this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1);
     };
 
@@ -205,9 +209,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
     docViewProps = () => ({
         ...this._props, //
         isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
-        isContentActive: this.isChildContentActive,
+        isContentActive: this._props.isContentActive,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
+    nativeScaling = () => this._props.NativeDimScaling?.() || 1;
     render() {
         return (
             <div
@@ -216,6 +221,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 style={{
                     background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+                    transformOrigin: 'top left',
+                    transform: `scale(${this.nativeScaling()})`,
+                    width: `${100 / this.nativeScaling()}%`,
+                    height: `${100 / this.nativeScaling()}%`,
                 }}>
                 <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}>
                     {this.content}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index ef66a2c83..ff587b199 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -58,18 +58,12 @@ export class CollectionCarouselView extends CollectionSubView() {
     /**
      * Goes to the next Doc in the stack subject to the currently selected filter option.
      */
-    advance = (e?: React.MouseEvent) => {
-        e?.stopPropagation();
-        this.move(1);
-    };
+    advance = () => this.move(1);
 
     /**
      * Goes to the previous Doc in the stack subject to the currently selected filter option.
      */
-    goback = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        this.move(-1);
-    };
+    goback = () => this.move(-1);
 
     curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
 
@@ -78,24 +72,23 @@ export class CollectionCarouselView extends CollectionSubView() {
         const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
         return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
     };
-    contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
-    contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
+    contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling();
+    contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling();
     onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
     captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
     contentScreenToLocalXf = () =>
         this._props
-            .ScreenToLocalTransform()
-            .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
-            .scale(this._props.NativeDimScaling?.() || 1);
+            .ScreenToLocalTransform() //
+            .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling());
     isChildContentActive = () =>
         this._props.isContentActive?.() === false
             ? false
-            : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+            : this._props.isContentActive()
               ? true
               : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
                 ? false
-                : undefined;
+                : undefined; // prettier-ignore
     onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
     renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
         return (
@@ -202,6 +195,8 @@ export class CollectionCarouselView extends CollectionSubView() {
         );
     }
 
+    nativeScaling = () => this._props.NativeDimScaling?.() || 1;
+
     docViewProps = () => ({
         ...this._props, //
         isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
@@ -212,20 +207,25 @@ export class CollectionCarouselView extends CollectionSubView() {
 
     render() {
         return (
-            <div
-                className="collectionCarouselView-outer"
-                ref={this.createDashEventsTarget}
-                style={{
-                    background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
-                    color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
-                    width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
-                    height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
-                    left: NumCast(this.layoutDoc._xMargin),
-                    top: NumCast(this.layoutDoc._yMargin),
-                }}>
-                {this.content}
-                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
-                {this.navButtons}
+            <div>
+                <div
+                    className="collectionCarouselView-outer"
+                    ref={this.createDashEventsTarget}
+                    style={{
+                        background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
+                        color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+                        left: NumCast(this.layoutDoc._xMargin),
+                        top: NumCast(this.layoutDoc._yMargin),
+                        transformOrigin: 'top left',
+                        transform: `scale(${this.nativeScaling()})`,
+                        width: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._xMargin)) / this.nativeScaling()}px)`,
+                        height: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._yMargin)) / this.nativeScaling()}px)`,
+                        position: 'relative',
+                    }}>
+                    {this.content}
+                    {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+                    {this.navButtons}
+                </div>
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index f85b0b433..ab5b70a85 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -523,7 +523,7 @@ export function CollectionSubView<X>() {
         /**
          * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
          */
-        @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+        @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
         /**
          * The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection
          * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7418d4360..6f0833a22 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -83,7 +83,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         return viewField;
     }
 
-    screenToLocalTransform = () => (this._props.renderDepth ? this.ScreenToLocalBoxXf() : this.ScreenToLocalBoxXf().scale(this._props.PanelWidth() / this.bodyPanelWidth()));
+    screenToLocalTransform = this.ScreenToLocalBoxXf;
     // prettier-ignore
     private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
         TraceMobx();
@@ -202,8 +202,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         }
     };
 
-    bodyPanelWidth = () => this._props.PanelWidth();
-
     childLayoutTemplate = () => this._props.childLayoutTemplate?.() || Cast(this.Document.childLayoutTemplate, Doc, null);
     isContentActive = () => this._isContentActive;
 
@@ -221,7 +219,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
             removeDocument: this.removeDocument,
             isContentActive: this.isContentActive,
             isAnyChildContentActive: this.isAnyChildContentActive,
-            PanelWidth: this.bodyPanelWidth,
+            PanelWidth: this._props.PanelWidth,
             PanelHeight: this._props.PanelHeight,
             ScreenToLocalTransform: this.screenToLocalTransform,
             childLayoutTemplate: this.childLayoutTemplate,
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 45e040653..79eb7f107 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
-import { IconButton, MultiToggle, Type } from 'browndash-components';
+import { MultiToggle, Type } from 'browndash-components';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -25,8 +25,8 @@ enum practiceVal {
 }
 
 export enum flashcardRevealOp {
-    HOVER = 'hover',
     FLIP = 'flip',
+    SLIDE = 'slide',
 }
 
 interface PracticeUIProps {
@@ -153,20 +153,28 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                     selectedItems={this.practiceMode}
                     onSelectionChange={(val: (string | number) | (string | number)[]) => togglePracticeMode(val as practiceMode)}
                 />
-                <IconButton
-                    tooltip="hover over card to reveal answer"
-                    type={Type.TERT}
-                    text={StrCast(this._props.layoutDoc.revealOp)}
+                <MultiToggle
+                    tooltip="How to reveal flashcard answer"
+                    type={Type.PRIM}
                     color={SnappingManager.userColor}
                     background={SnappingManager.userVariantColor}
-                    icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? 'hand-point-up' : 'question'} size="sm" />}
-                    label={StrCast(this._props.layoutDoc.revealOp)}
-                    onPointerDown={e =>
-                        setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => {
-                            this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.HOVER ? flashcardRevealOp.FLIP : flashcardRevealOp.HOVER;
-                            this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined;
-                        })
-                    }
+                    multiSelect={false}
+                    isToggle={false}
+                    toggleStatus={!!this.practiceMode}
+                    label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)}
+                    items={[
+                        ['reveal', StrCast(this._props.layoutDoc.revealOp) === flashcardRevealOp.SLIDE ? 'expand' : 'question', StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)],
+                        ['trigger', this._props.layoutDoc.revealOp_hover ? 'hand-point-up' : 'hand', this._props.layoutDoc.revealOp_hover ? 'show on hover' : 'show on click'],
+                    ].map(([item, icon, tooltip]) => ({
+                        icon: <FontAwesomeIcon className={`FlashcardPracticeUI-${item}`} color={setColor(item as practiceMode)} icon={icon as IconProp} size="sm" />,
+                        tooltip: tooltip,
+                        val: item,
+                    }))}
+                    selectedItems={this._props.layoutDoc.revealOp_hover ? ['reveal', 'trigger'] : 'reveal'}
+                    onSelectionChange={(val: (string | number) | (string | number)[]) => {
+                        if (val === 'reveal') this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.SLIDE ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE;
+                        if (val === 'trigger') this._props.layoutDoc.revealOp_hover = !this._props.layoutDoc.revealOp_hover;
+                    }}
                 />
             </div>
         );
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index c328ef4bf..d1cc48051 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -246,8 +246,7 @@
     pointer-events: none;
 }
 
-.comparisonBox-interactive {
-    pointer-events: unset;
+.comparisonBox-slide {
     cursor: ew-resize;
 
     .slide-bar {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 80ef126dc..38ce5f2f7 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -35,22 +35,21 @@ import { FormattedTextBox } from './formattedText/FormattedTextBox';
 const API_URL = 'https://api.unsplash.com/search/photos';
 
 /**
- * This view serves three distinct functions depending on the metadata field layout_isFlashcard
- *     1) it provides a before/after animated sliding transition between two Docs
- *     2) it provides a question/answer switch between two Docs (flashcard)
- *     3) it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
+ * This view serves two distinct functions depending on the revealOp field ('slide' or 'flip)
+ *     1) ('slide') - provides a before/after animated sliding transition between two Docs
+ *     2) ('flip') - provides a question/answer flip between two Docs
+ * And a third function that overrides the first two if the doc's container has its 'practiceMode' set to 'quiz'
+ *     3) ('quiz') - it provides a quiz view that displays a question and a user answer that can be "scored" by GPT
+ *     NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
  *
  * In each case, the two docs are stored in the <fieldKey>_front and <fieldKey>_back fields
  *
- * In the case of the flashcard, there is an icon that allows the user to choose between a
- * hover and a flip action to switch between cards.  The transition is stored in the 'revealOp' field.
- * In addition, if a flashcard is created without data in the front/back fields, this will
- * create Text documents with placeholder text indicating to the user how to fill in the cards.
- * One option is to allow the user to enter a topic and, by clicking on the flashcard stack button,
- * convert the comparision box into a stack of comparison boxes filled in by GPT about the topic.
+ * For 'flip' and 'slide', the trigger can either be clicking, or hovering as determined by the revealOp_hover field.
+ * For 'quiz' the data of both Docs are shown in a single-view quiz display.
  *
- * Quiz mode is activated when the parent collection has its 'quiz' field set when it renders a flashcard.
- * NOTE: this should probably be changed to passing down a prop to the flashcard telling it to render as a quiz.
+ * Users can create a stack of flashcards all at once (only) from an empty flashcard by entering a topic into the front card
+ * and clicking on the flashcard stack button.  This will convert the comparision box into a stack of comparison boxes
+ * filled in by GPT about the topic.
  *
  */
 
@@ -67,7 +66,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      */
     public static createFlashcard(tuple: string, frontKey: string, backKey: string, useDoc?: Doc) {
         const [ktoken, atoken] = [ComparisonBox.ktoken, ComparisonBox.atoken];
-        const newDoc = useDoc ?? Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 });
         const question = (tuple.includes(ktoken) ? tuple.split(ktoken)[0] : tuple).split(atoken)[0];
         const rest = tuple.replace(question, '');
         // prettier-ignore
@@ -78,9 +76,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             rest.replace(atoken,""); // finally if there's no keyword, just get rid of answer token and take what's left
         const keyword = rest.replace(atoken, '').replace(answer, '').replace(ktoken, '').trim();
         const fillInFlashcard = (img?: Doc) => {
-            newDoc[DocData][frontKey] = FormattedTextBox.centeredTextCreator('question', question, img);
-            newDoc[DocData][backKey] = FormattedTextBox.centeredTextCreator('answer', answer);
-            return newDoc;
+            const front = Docs.Create.CenteredTextCreator('question', question, {}, img);
+            const back = Docs.Create.CenteredTextCreator('answer', answer, {});
+            if (useDoc) {
+                useDoc[DocData][frontKey] = front;
+                useDoc[DocData][backKey] = back;
+                return useDoc;
+            }
+            return Docs.Create.FlashcardDocument('flashcard', front, back, { _width: 300, _height: 300 });
         };
         return keyword && keyword.toLowerCase() !== 'none' ? ComparisonBox.fetchImages(keyword).then(img => fillInFlashcard(img)) : fillInFlashcard();
     }
@@ -98,6 +101,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 .map(tuple => ComparisonBox.createFlashcard(tuple, front, back))
         ).then(docs => {
             return Docs.Create.CarouselDocument(docs, {
+                title: 'flashcard deck',
                 _width: width,
                 _height: height,
                 _layout_fitWidth: false,
@@ -116,12 +120,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     private _sideBtnWidth = 35;
     private _closeRef = React.createRef<HTMLDivElement>();
     private _disposers: { [key: string]: DragManager.DragDropDisposer | undefined } = {};
-    private _reactDisposer: IReactionDisposer | undefined;
+    private _reactDisposer: { [key: string]: IReactionDisposer } = {};
 
     @observable private _inputValue = '';
     @observable private _outputValue = '';
     @observable private _loading = false;
-    @observable private _isEmpty = false;
     @observable private _childActive = false;
     @observable private _animating = '';
     @observable private _listening = false;
@@ -135,18 +138,28 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._reactDisposer = reaction(
-            () => this._props.isSelected(), // when this reaction should update
+        this._reactDisposer.select = reaction(
+            () => this._props.isSelected(),
             selected => {
-                if (selected && this.isFlashcard) this.activateContent();
+                if (selected && this.revealOp !== flashcardRevealOp.SLIDE) this.activateContent();
                 !selected && (this._childActive = false);
             }, // what it should update to
             { fireImmediately: true }
         );
+        this._reactDisposer.hover = reaction(
+            () => this._props.isContentActive(),
+            hover => {
+                if (!hover) {
+                    this.revealOp === flashcardRevealOp.FLIP && this.animateFlipping(this.frontKey);
+                    this.revealOp === flashcardRevealOp.SLIDE && this.animateSliding(this._props.PanelWidth() - 3);
+                }
+            }, // what it should update to
+            { fireImmediately: true }
+        );
     }
 
     componentWillUnmount() {
-        this._reactDisposer?.();
+        Object.values(this._reactDisposer).forEach(disposer => disposer?.());
     }
 
     protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string) => {
@@ -169,7 +182,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         return undefined;
     }, 'internal drop');
 
-    @computed get isQuizMode()    { return DocCast(this.Document.embedContainer)?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
+    @computed get containerDoc()  { return this._props.docViewPath().slice(-2)[0]?.Document; } // prettier-ignore
+    @computed get isQuizMode()    { return this.containerDoc?.practiceMode === practiceMode.QUIZ; } // prettier-ignore
     @computed get isFlashcard()   { return BoolCast(this.Document.layout_isFlashcard); } // prettier-ignore
     @computed get frontKey()      { return this._props.fieldKey + '_front'; } // prettier-ignore
     @computed get backKey()       { return this._props.fieldKey + '_back'; } // prettier-ignore
@@ -178,10 +192,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get revealOpKey()   { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
     @computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
     @computed get clipWidthKey()  { return `_${this._props.fieldKey}_clipWidth`;  } // prettier-ignore
-    @computed get clipWidth()     { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } // prettier-ignore
+    @computed get clipWidth()     { return NumCast(this.layoutDoc[this.clipWidthKey], this.isFlashcard ? 100: 50); } // prettier-ignore
     @computed get clipHeight()    { return NumCast(this.layoutDoc[this.clipHeightKey], 200);  } // prettier-ignore
-    @computed get revealOp()      { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this._props.docViewPath().slice(-2)[0]?.Document.revealOp)); } // prettier-ignore
-    set revealOp(value:string)    { this.layoutDoc[this.revealOpKey] = value; } // prettier-ignore
+    @computed get revealOp()      { return StrCast(this.layoutDoc[this.revealOpKey], StrCast(this.containerDoc?.revealOp, this.isFlashcard ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE)) as flashcardRevealOp; } // prettier-ignore
+    @computed get revealOpHover() { return BoolCast(this.layoutDoc[this.revealOpKey+"_hover"], BoolCast(this.containerDoc?.revealOp_hover)); } // prettier-ignore
     @computed get loading()       { return this._loading; } // prettier-ignore
     set loading(value)            { runInAction(() => { this._loading = value; })} // prettier-ignore
 
@@ -193,13 +207,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     onPointerDown={e =>
                         setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => {
                             if (!this.revealOp || this.revealOp === flashcardRevealOp.FLIP) {
-                                this.flipFlashcard();
+                                this.animateFlipping();
                             }
                         })
                     }
                     style={{
-                        background: this.revealOp === flashcardRevealOp.HOVER ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black',
-                        color: this.revealOp === flashcardRevealOp.HOVER ? 'black' : this._renderSide === this.backKey ? 'black' : 'white',
+                        background: this.revealOpHover ? 'gray' : this._renderSide === this.backKey ? 'white' : 'black',
+                        color: this.revealOpHover ? 'black' : this._renderSide === this.backKey ? 'black' : 'white',
                         display: 'inline-block',
                     }}>
                     <FontAwesomeIcon icon="turn-up" size="xl" />
@@ -223,7 +237,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get flashcardMenu() {
         return SnappingManager.HideDecorations ? null : (
             <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
-                {this.revealOp === flashcardRevealOp.HOVER || !this._props.isSelected() ? null : this.overlayAlternateIcon}
+                {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon}
                 {!this._props.isSelected() || this._renderSide === this.frontKey ? null : (
                     <Tooltip title={<div className="dash-tooltip">Ask GPT to create an answer for the question on the front</div>}>
                         <div className="comparisonBox-button" onPointerDown={() => this.askGPT(GPTCallType.CHATCARD)}>
@@ -296,13 +310,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
     moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc);
     addDoc = (doc: Doc, which: string) => {
-        if (this.dataDoc[which] && !this._isEmpty) return false;
         this.dataDoc[which] = doc;
         return true;
     };
     remDoc = (doc: Doc, which: string) => {
         if (this.dataDoc[which] === doc) {
-            this._isEmpty = true;
             this.dataDoc[which] = undefined;
             return true;
         }
@@ -334,6 +346,26 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     moveDocBack = (docs: Doc | Doc[], targetCol: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => toList(docs).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.backKey), true);
     remDocFront = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.frontKey), true);
     remDocBack = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.backKey), true);
+    animateSliding = action((targetWidth: number) => {
+        this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth
+        this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
+        setTimeout(action(() => {this._animating = '';  }), this._slideTiming); // prettier-ignore
+    });
+
+    _flipAnim: NodeJS.Timeout | undefined;
+    animateFlipping = action((side?: string) => {
+        if (side !== this._renderSide) {
+            this._renderSide = side ?? (this._renderSide === this.frontKey ? this.backKey : this.frontKey); // switches to new front
+            this._animating = '0'; // reveals old front on the bottom layer by making top layer transparent
+            setTimeout(
+                action(() => {
+                    this._animating = `all ${this._slideTiming * 5}ms`; // makes new front fade in
+                    clearTimeout(this._flipAnim);
+                    this._flipAnim = setTimeout(  action(() => { this._animating = '';  }), this._slideTiming * 5 ); // prettier-ignore
+                })
+            );
+        }
+    });
 
     registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
         if (e.button !== 2) {
@@ -351,13 +383,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                 }),
                 false,
                 undefined,
-                action(() => {
-                    if (!this._childActive) {
-                        this._animating = `all ${this._slideTiming}ms`; // on click, animate slider movement to the targetWidth
-                        this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
-                        setTimeout(  action(() => {this._animating = '';  }), this._slideTiming); // prettier-ignore
-                    }
-                })
+                action(() => !this._childActive && this.animateSliding(targetWidth))
             );
         }
     };
@@ -584,16 +610,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         }
     };
 
-    @action
-    flipFlashcard = () => {
-        this._renderSide = this._renderSide === this.frontKey ? this.backKey : this.frontKey;
-    };
-
-    @action
-    hoverFlip = (side: string) => {
-        if (this.revealOp === flashcardRevealOp.HOVER) this._renderSide = side;
-    };
-
     flashcardContextMenu = () => {
         const appearance = ContextMenu.Instance.findByDescription('Appearance...');
         const appearanceItems = appearance?.subitems ?? [];
@@ -680,16 +696,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     };
 
     displayBox = (which: string, cover: number) => (
-        <div
-            className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`}
-            key={which}
-            style={{ width: this._props.PanelWidth() }}
-            onPointerDown={e => {
-                this.registerSliding(e, cover);
-                this.isFlashcard && this.activateContent();
-            }}
-            ref={ele => this.createDropTarget(ele, which)}>
-            {!this._isEmpty ? this.displayDoc(which) : null}
+        <div className={`${which === this.frontKey ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which)}>
+            {this.displayDoc(which)}
         </div>
     );
 
@@ -727,7 +735,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                     <button className="submit-buttonpronunciation" onClick={this.evaluatePronunciation}>
                         Evaluate Pronunciation
                     </button>
-                    <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? this.flipFlashcard : this.handleRenderGPTClick}>
+                    <button className="submit-buttonsubmit" type="button" onClick={this._renderSide === this.backKey ? () => this.animateFlipping(this.frontKey) : this.handleRenderGPTClick}>
                         {this._renderSide === this.backKey ? 'Redo the Question' : 'Submit'}
                     </button>
                 </div>
@@ -736,40 +744,33 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     );
 
     // if flashcard is rendered that has no data, then add some placeholders for question and answer
-    addPlaceholdersForEmptyFlashcard = () => {
-        if (this.dataDoc.data) {
-            if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document);
-        } else {
-            // add text box to each side when comparison box is first created
-            if (!this.dataDoc[this.backKey] && !this._isEmpty) {
-                this.dataDoc[this.backKey] = FormattedTextBox.centeredTextCreator('answer', 'answer here', undefined, true);
-            }
-
-            if (!this.dataDoc[this.frontKey] && !this._isEmpty) {
-                this.dataDoc[this.frontKey] = FormattedTextBox.centeredTextCreator('question', 'hint: Enter a topic, select this document and click the stack button to have GPT create a deck of cards', undefined, true);
-            }
-        }
-    };
-
-    renderAsFlashcard = () => (
+    // addPlaceholdersForEmptyFlashcard = () => {
+    //     if (this.dataDoc.data) {
+    //         if (!this.dataDoc[this.backKey] || !this.dataDoc[this.frontKey]) ComparisonBox.createFlashcard(StrCast(this.dataDoc.data), this.frontKey, this.backKey, this.Document);
+    //     }
+    // };
+
+    // render a button that flips between front and back
+    renderAsFlip = () => (
         <div
-            className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
-            onContextMenu={this.flashcardContextMenu}
-            onMouseEnter={() => this.hoverFlip(this.backKey)}
-            onMouseLeave={() => this.hoverFlip(this.frontKey)}>
-            {this.displayBox(this._renderSide, this._props.PanelWidth() - 3)}
-            {this.loading ? (
-                <div className="loading-spinner">
-                    <ReactLoading type="spin" height={30} width={30} color="blue" />
-                </div>
-            ) : null}
+            style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }} //
+            onMouseEnter={() => this.revealOpHover && this.animateFlipping(this.backKey)}
+            onMouseLeave={() => this.revealOpHover && this.animateFlipping(this.frontKey)}>
+            <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 1 : 0 }}>
+                {this.displayBox(this._renderSide === this.backKey ? this.frontKey : this.backKey, 0)}
+            </div>
+            <div style={{ position: 'absolute', width: '100%', height: '100%', transition: this._animating === '0' ? undefined : this._animating, opacity: this._animating === '0' ? 0 : 1 }}>{this.displayBox(this._renderSide, 0)}</div>
             {this.flashcardMenu}
         </div>
     );
 
-    // render a comparison box that compares items side by side
+    // render a slider that reveals front and back as slider is dragged horizonally
     renderAsBeforeAfter = () => (
-        <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
+        <div
+            className="comparisonBox-slide"
+            style={{ display: 'flex', pointerEvents: this.revealOpHover && this._props.isContentActive() ? 'unset' : undefined }}
+            onMouseEnter={() => this.revealOpHover && this.animateSliding(0)}
+            onMouseLeave={() => this.revealOpHover && this.animateSliding(this._props.PanelWidth() - 3)}>
             {this.displayBox(this.backKey, this._props.PanelWidth() - 3)}
             <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
                 {this.displayBox(this.frontKey, 0)}
@@ -789,11 +790,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     );
 
     render() {
-        this.isFlashcard && this.addPlaceholdersForEmptyFlashcard();
-        return this.isFlashcard ? 
-                    this.isQuizMode ? this.renderAsQuiz(this.frontText) :
-                                      this.renderAsFlashcard() : 
-                    this.renderAsBeforeAfter(); // prettier-ignore
+        const renderMode = new Map<flashcardRevealOp, () => JSX.Element>([
+            [flashcardRevealOp.FLIP, this.renderAsFlip],
+            [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
+        if (this.isQuizMode) this.renderAsQuiz(this.frontText);
+        return (
+            <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
+                {renderMode.get(this.revealOp)?.() ?? null}
+                {this.loading ? (
+                    <div className="loading-spinner">
+                        <ReactLoading type="spin" height={30} width={30} color="blue" />
+                    </div>
+                ) : null}
+            </div>
+        );
     }
 }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9d3a899f5..29be8d285 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -76,26 +76,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
     public static LayoutString(fieldStr: string) {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
-    /**
-     * Creates a text box where the supplied text (and optional iimage) will be vertically
-     * and horizontally centered.  If text_placeholder is set to true, then the text will be
-     * treated as placeholder text and automatically selected when the text box is selected.
-     * @param title name of text box
-     * @param text text to display in textbox
-     * @param img optional image to add to text box
-     * @param text_placeholder makes the text automatially select
-     * @returns
-     */
-    public static centeredTextCreator(title: string, text: string, img?: Doc, text_placeholder?: boolean) {
-        return Docs.Create.TextDocument(RichTextField.textToRtf(text, img?.[Id]), {
-            title, //
-            _layout_autoHeight: true,
-            _layout_centered: true,
-            text_align: 'center',
-            text_placeholder,
-            _layout_fitWidth: true,
-        });
-    }
     public static MakeConfig(rules?: RichTextRules, props?: FormattedTextBoxProps) {
         return {
             schema,
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 45dfe233f..6ec195910 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1003,7 +1003,7 @@ export namespace Doc {
                     } else if (field instanceof ObjectField) {
                         const docAtKey = doc[key];
                         copy[key] =
-                            docAtKey instanceof Doc && key.includes('layout[')
+                            docAtKey instanceof Doc && (key.includes('layout[') || docAtKey.cloneOnCopy)
                                 ? new ProxyField(Doc.MakeCopy(docAtKey)) // copy the expanded render template
                                 : ObjectField.MakeCopy(field);
                     } else if (field instanceof Promise) {
-- 
cgit v1.2.3-70-g09d2


From fdedab74b886f0a83d5a22b39620758adec129c0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Oct 2024 19:58:51 -0400
Subject: fixed size of flashcard ui (and other buttons) when fitWidth is set
 and when Doc is resized small and zoomed out.

---
 src/client/views/collections/CollectionSubView.tsx   | 2 +-
 src/client/views/collections/FlashcardPracticeUI.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index ab5b70a85..c27fd0dae 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -529,7 +529,7 @@ export function CollectionSubView<X>() {
          * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
          * size or a fraction of the collection view.
          */
-        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) ? 1:  0.25) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
         /**
          * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
          * Note, the scale factor does not allow for elements to grow larger than their native screen space size.
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index ec892ee3d..9e9318c0a 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -131,7 +131,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             <div
                 className="FlashcardPracticeUI-practiceModes"
                 style={{
-                    transform: `translateY(${(this.btnHeight() * (1 - Math.min(1, this._props.uiBtnScaling))) / this._props.ScreenToLocalBoxXf().Scale}px)`,
+                    transform: this._props.ScreenToLocalBoxXf().Scale >= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`,
                 }}>
                 <MultiToggle
                     tooltip="Practice flashcards one at a time"
-- 
cgit v1.2.3-70-g09d2


From 6619175b1468b343272a7a4c36d73fefc1c6b431 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Oct 2024 20:12:20 -0400
Subject: from last

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

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index c27fd0dae..48aac3a68 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -529,7 +529,7 @@ export function CollectionSubView<X>() {
          * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
          * size or a fraction of the collection view.
          */
-        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) ? 1:  0.25) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1:  0.25) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
         /**
          * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
          * Note, the scale factor does not allow for elements to grow larger than their native screen space size.
-- 
cgit v1.2.3-70-g09d2


From 0728e918e6d075c0eda738b7d2fdbf6095c714ae Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 18 Oct 2024 00:28:55 -0400
Subject: fixed following links/show document of items in tabs/carousel/card
 view to set options.didMove correctly so that toggleTarget works.

---
 src/client/documents/Documents.ts                  |  2 ++
 .../views/collections/CollectionCardDeckView.tsx   | 42 +++++++++-------------
 .../views/collections/CollectionCarousel3DView.tsx | 40 +++++++--------------
 .../views/collections/CollectionCarouselView.tsx   | 33 ++++++-----------
 src/client/views/collections/CollectionSubView.tsx | 14 +++++++-
 src/client/views/collections/TabDocView.tsx        |  1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 19 ++++------
 src/client/views/nodes/trails/PresBox.tsx          |  3 +-
 src/extensions/ExtensionsTypings.ts                |  8 +++++
 src/extensions/Extensions_Array.ts                 | 10 ++++--
 10 files changed, 78 insertions(+), 94 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f2e85cf7a..be3b9c3bb 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -372,6 +372,8 @@ export class DocumentOptions {
     config_panX?: NUMt = new NumInfo('panX saved as a view spec', false);
     config_panY?: NUMt = new NumInfo('panY saved as a view spec', false);
     config_zoom?: NUMt = new NumInfo('zoom saved as a view spec', false);
+    config_carousel_index?: NUMt = new NumInfo('saved carousel index', false);
+    config_curDoc?: DOCt = new DocInfo('current doc in a collection view, e.g., cardView');
     config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false);
     presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false);
     presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 60305244c..729a1585a 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -9,25 +9,24 @@ import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { URLField } from '../../../fields/URLField';
 import { gptImageLabel } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoable } from '../../util/UndoManager';
+import { PinDocView } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { Docs } from '../../documents/Documents';
-import { PinDocView } from '../PinFuncs';
-import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
 enum cardSortings {
     Time = 'time',
@@ -599,22 +598,21 @@ export class CollectionCardView extends CollectionSubView() {
     });
 
     focus = action((anchor: Doc, options: FocusViewOptions): Opt<number> => {
-        const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
-        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
-        options.didMove = true;
-        const target = DocCast(anchor.annotationOn) ?? anchor;
-        const index = docs.indexOf(target);
-        index !== -1 && (this._curDoc = target);
+        const docs = DocListCast(this.Document[this.fieldKey]);
+        if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+            const foundDoc = DocCast(
+                anchor.config_curDoc,
+                docs.find(doc => doc === DocCast(anchor.annotationOn, anchor))
+            );
+            options.didMove = foundDoc !== this.curDoc() ? true : false;
+            options.didMove && (this._curDoc = foundDoc);
+        }
         return undefined;
     });
     getAnchor = (addAsAnnotation: boolean) => {
-        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
-        anchor.config_curDoc = this.curDoc();
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_curDoc: this.curDoc() });
         PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
-        if (addAsAnnotation) {
-            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
-            Doc.AddDocToList(this.dataDoc, this._props.fieldKey + '_annotations', anchor);
-        }
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
     // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
@@ -625,15 +623,7 @@ export class CollectionCardView extends CollectionSubView() {
         }
         return false;
     };
-    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
-        const doc = toList(docsIn).lastElement();
-        const where = location.split(':')[0];
-        if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
-            if (doc.hidden) doc.hidden = false;
-            if (!location.includes(OpenWhereMod.always)) return true;
-        }
-        return this._props.addDocTab(docsIn, location);
-    };
+    addDocTab = this.addLinkedDocTab;
 
     /**
      * Actually renders all the cards
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 81e921724..081e2fe5a 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,24 +1,23 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { returnZero } from '../../../ClientUtils';
 import { Utils } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { BoolCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Transform } from '../../util/Transform';
+import { PinDocView } from '../PinFuncs';
 import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
 import './CollectionCarousel3DView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { computedFn } from 'mobx-utils';
-import { Docs } from '../../documents/Documents';
-import { PinDocView } from '../PinFuncs';
-import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
 // eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@@ -94,22 +93,18 @@ export class CollectionCarousel3DView extends CollectionSubView() {
             .scale(1 / this.centerScale);
 
     focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
-        const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
-        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
-        options.didMove = true;
-        const target = DocCast(anchor.annotationOn) ?? anchor;
-        const index = docs.indexOf(target);
-        index !== -1 && (this.layoutDoc._carousel_index = index);
+        const docs = DocListCast(this.Document[this.fieldKey]);
+        if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+            const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+            options.didMove = newIndex !== this.layoutDoc._carousel_index;
+            options.didMove && (this.layoutDoc._carousel_index = newIndex);
+        }
         return undefined;
     };
     getAnchor = (addAsAnnotation: boolean) => {
-        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
-        anchor.config_carousel_index = this.layoutDoc._carousel_index;
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
         PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
-        if (addAsAnnotation) {
-            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
-            Doc.AddDocToList(this.dataDoc, this._props.fieldKey + '_annotations', anchor);
-        }
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
     // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
@@ -120,16 +115,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         }
         return false;
     };
-    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
-        const doc = toList(docsIn).lastElement();
-        const where = location.split(':')[0];
-        if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
-            if (doc.hidden) doc.hidden = false;
-            if (!location.includes(OpenWhereMod.always)) return true;
-        }
-        return this._props.addDocTab(docsIn, location);
-    };
-
+    addDocTab = this.addLinkedDocTab;
     @computed get content() {
         const currentIndex = NumCast(this.layoutDoc._carousel_index);
         const displayDoc = (child: Doc, dxf: () => Transform) => (
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index b043706fd..bfb58733d 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { BoolCast, DocCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
@@ -13,7 +13,6 @@ import { StyleProp } from '../StyleProp';
 import { DocumentView } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -76,23 +75,19 @@ export class CollectionCarouselView extends CollectionSubView() {
     curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
 
     focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
-        const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
-        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
-        options.didMove = true;
-        const target = DocCast(anchor.annotationOn) ?? anchor;
-        const index = docs.indexOf(target);
-        index !== -1 && (this.layoutDoc._carousel_index = index);
+        const docs = DocListCast(this.Document[this.fieldKey]);
+        if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+            const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+            options.didMove = newIndex !== this.layoutDoc._carousel_index;
+            options.didMove && (this.layoutDoc._carousel_index = newIndex);
+        }
         return undefined;
     };
 
     getAnchor = (addAsAnnotation: boolean) => {
-        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document });
-        anchor.config_carousel_index = this.carouselIndex;
+        const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
         PinDocView(anchor, { pinData: { type_collection: true, filters: true } }, this.Document);
-        if (addAsAnnotation) {
-            // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
-            Doc.AddDocToList(this.dataDoc, this._props.fieldKey + '_annotations', anchor);
-        }
+        addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
         return anchor;
     };
     // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
@@ -103,15 +98,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         }
         return false;
     };
-    addDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
-        const doc = toList(docsIn).lastElement();
-        const where = location.split(':')[0];
-        if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
-            if (doc.hidden) doc.hidden = false;
-            if (!location.includes(OpenWhereMod.always)) return true;
-        }
-        return this._props.addDocTab(docsIn, location);
-    };
+    addDocTab = this.addLinkedDocTab;
 
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
         // first look for properties on the document in the carousel, then fallback to properties on the container
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 48aac3a68..9952863d7 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -27,6 +27,7 @@ import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldViewProps } from '../nodes/FieldView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FlashcardPracticeUI } from './FlashcardPracticeUI';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -130,6 +131,17 @@ export function CollectionSubView<X>() {
         @computed get childDocList() {
             return Cast(this.dataField, listSpec(Doc));
         }
+
+        addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+            const doc = toList(docsIn).lastElement();
+            const where = location.split(':')[0];
+            if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+                if (doc.hidden) doc.hidden = false;
+                if (!location.includes(OpenWhereMod.always)) return true;
+            }
+            return this._props.addDocTab(docsIn, location);
+        };
+
         collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters);
         collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []);
         // child filters apply to the descendants of the documents in this collection
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5bfdee1f5..1c5722217 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -285,6 +285,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
 
     static Activate = (tabDoc: Doc) => {
         const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
+        if (tab.header.parent._activeContentItem === tab.contentItem) return false;
         tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         return tab !== undefined;
     };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index afd662b22..0241fc2dd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -40,7 +40,7 @@ import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, Active
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FocusViewOptions } from '../../nodes/FocusViewOptions';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere';
+import { OpenWhere } from '../../nodes/OpenWhere';
 import { PinDocView, PinProps } from '../../PinFuncs';
 import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
 import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
@@ -1596,21 +1596,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
             case undefined:
             case OpenWhere.lightbox:
-                {
-                    const firstDoc = docs[0];
-                    if (this.layoutDoc._isLightbox) {
-                        this._lightboxDoc = firstDoc;
-                        return true;
-                    }
-                    if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) {
-                        if (firstDoc.hidden) firstDoc.hidden = false;
-                        if (!location.includes(OpenWhereMod.always)) return true;
-                    }
+                if (this.layoutDoc._isLightbox) {
+                    this._lightboxDoc = docs[0];
+                    return true;
                 }
-                break;
+                return this.addLinkedDocTab(docsIn, location);
             default:
         }
-        return this._props.addDocTab(docs, location);
+        return this._props.addDocTab(docsIn, location);
     });
     getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData {
         const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 7448fa898..53d72e946 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -48,6 +48,7 @@ import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import SlideEffect from './SlideEffect';
 import { AnimationSettings, SpringSettings, SpringType, easeItems, effectItems, effectTimings, movementItems, presEffectDefaultTimings, springMappings, springPreviewColors } from './SpringUtils';
+import _ from 'lodash';
 
 @observer
 export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -708,7 +709,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
 
         if ((pinDataTypes?.filters && activeItem.config_docFilters !== undefined) || (!pinDataTypes && activeItem.config_docFilters !== undefined)) {
-            if (bestTarget.childFilters !== activeItem.config_docFilters) {
+            if (!_.isEqual(Array.from(StrListCast(bestTarget.childFilters)), Array.from(StrListCast(activeItem.config_docFilters)))) {
                 bestTarget.childFilters = ObjectField.MakeCopy(activeItem.config_docFilters as ObjectField) || new List<string>([]);
                 changed = true;
             }
diff --git a/src/extensions/ExtensionsTypings.ts b/src/extensions/ExtensionsTypings.ts
index d6ffd3be3..fa8851bb3 100644
--- a/src/extensions/ExtensionsTypings.ts
+++ b/src/extensions/ExtensionsTypings.ts
@@ -1,6 +1,14 @@
 /* eslint-disable @typescript-eslint/no-unused-vars */
 interface Array<T> {
+    /**
+     * returns the last element of the array or undefined
+     */
     lastElement(): T;
+    /**
+     * if val is in the list, it returns its index, otherwise undefined;
+     * @param val
+     */
+    getIndex(val: T): number | undefined;
 }
 
 interface String {
diff --git a/src/extensions/Extensions_Array.ts b/src/extensions/Extensions_Array.ts
index a50fb330f..d61585e28 100644
--- a/src/extensions/Extensions_Array.ts
+++ b/src/extensions/Extensions_Array.ts
@@ -1,14 +1,13 @@
 export default class ArrayExtension {
     private readonly property: string;
-    private readonly body: <T>(this: Array<T>) => any;
+    private readonly body: <T>(this: Array<T>, args: unknown) => unknown;
 
-    constructor(property: string, body: <T>(this: Array<T>) => any) {
+    constructor(property: string, body: <T>(this: Array<T>, args: unknown) => unknown) {
         this.property = property;
         this.body = body;
     }
 
     assign() {
-        // eslint-disable-next-line no-extend-native
         Object.defineProperty(Array.prototype, this.property, {
             value: this.body,
             enumerable: false,
@@ -28,6 +27,11 @@ const extensions = [
         }
         return this[this.length - 1];
     }),
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    new ArrayExtension('getIndex', function (val: any) {
+        const index = this.indexOf(val);
+        return index === -1 ? undefined : index;
+    }),
 ];
 
 function Assign() {
-- 
cgit v1.2.3-70-g09d2


From 050fe4fe0d9f9722a7a2a48ae3b446049f93775f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 24 Oct 2024 11:06:27 -0400
Subject: fixed doc decorations for carousel docs.

---
 .../views/collections/CollectionCarousel3DView.tsx       | 16 +++++++++++-----
 src/client/views/collections/CollectionSubView.tsx       |  3 ++-
 2 files changed, 13 insertions(+), 6 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 61a74dd7a..c080ba27e 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -75,21 +75,27 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                       ? false
                       : undefined
     );
-    contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+    contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().translate(0, (-(Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) / this.nativeScaling());
     childScreenLeftToLocal = () =>
         this.contentScreenToLocalXf()
-            .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+            .translate(
+                (-this.panelWidth() * (1 - this.sideScale)) / 2, //
+                (-this.panelHeight() * (1 - this.sideScale)) / 2
+            )
             .scale(1 / this.sideScale);
     childScreenRightToLocal = () =>
         this.contentScreenToLocalXf()
-            .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+            .translate(
+                -2 * this.panelWidth() - (this.panelWidth() * (1 - this.sideScale)) / 2, //
+                (-this.panelHeight() * (1 - this.sideScale)) / 2
+            )
             .scale(1 / this.sideScale);
     childCenterScreenToLocal = () =>
         this.contentScreenToLocalXf()
             .translate(
                 -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
-                -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
-            ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2
+                ((this.centerScale - 1) * this.panelHeight()) / 2
+            )
             .scale(1 / this.centerScale);
 
     focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9952863d7..0c059f729 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -531,6 +531,7 @@ export function CollectionSubView<X>() {
         };
 
         protected _sideBtnWidth = 35;
+        protected _sideBtnMaxPanelPct = 0.15;
         @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
         /**
          * How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
@@ -541,7 +542,7 @@ export function CollectionSubView<X>() {
          * This takes the desired screen space size and converts into collection coordinates.  It then returns the smaller of the converted
          * size or a fraction of the collection view.
          */
-        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1:  0.25) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
+        @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1));  } // prettier-ignore
         /**
          * This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
          * Note, the scale factor does not allow for elements to grow larger than their native screen space size.
-- 
cgit v1.2.3-70-g09d2


From a496c583f375fe5e118f786b1244c42bc8a34fec Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Jan 2025 14:20:40 -0500
Subject: fixed flashcard practice mode.  moved doc sorting from cardDeck into
 collectionSubView to apply it to all collections.

---
 .../src/components/Dropdown/Dropdown.tsx           |  30 ++----
 src/client/util/CurrentUserUtils.ts                |  10 +-
 .../views/collections/CollectionCardDeckView.tsx   | 119 ++++-----------------
 src/client/views/collections/CollectionSubView.tsx |  49 ++++++++-
 .../views/collections/FlashcardPracticeUI.tsx      |   4 +-
 src/client/views/global/globalScripts.ts           |  35 +++---
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/imageEditor/ImageEditor.tsx |  69 ++----------
 .../views/nodes/imageEditor/ImageEditorButtons.tsx |   1 +
 9 files changed, 108 insertions(+), 211 deletions(-)

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

diff --git a/packages/components/src/components/Dropdown/Dropdown.tsx b/packages/components/src/components/Dropdown/Dropdown.tsx
index 0953f412c..b9b6f01b8 100644
--- a/packages/components/src/components/Dropdown/Dropdown.tsx
+++ b/packages/components/src/components/Dropdown/Dropdown.tsx
@@ -99,22 +99,14 @@ export const Dropdown = (props: IDropdownProps) => {
         background: background ?? color,
     };
 
-    const getCaretDirection = (active: boolean, placement: Placement = 'left'): JSX.Element => {
-        if (iconProvider) return iconProvider(active, placement);
-        switch (placement) {
-            case 'bottom':
-                if (active) return <FaCaretUp />;
-                return <FaCaretDown />;
-            case 'right':
-                if (active) return <FaCaretLeft />;
-                return <FaCaretRight />;
-            case 'top':
-                if (active) return <FaCaretDown />;
-                return <FaCaretUp />;
-            default:
-                if (active) return <FaCaretUp />;
-                return <FaCaretDown />;
-        }
+    const getCaretDirection = (isActive: boolean, caretPlacement: Placement = 'left'): JSX.Element => {
+        if (iconProvider) return iconProvider(isActive, caretPlacement);
+        switch (caretPlacement) {
+            default:      
+            case 'bottom':return isActive ?  <FaCaretUp />: <FaCaretDown />;
+            case 'right': return isActive ? <FaCaretLeft /> : <FaCaretRight />;
+            case 'top':   return isActive ? <FaCaretDown />: <FaCaretUp />;
+        } // prettier-ignore
     };
 
     const getToggle = () => {
@@ -143,9 +135,9 @@ export const Dropdown = (props: IDropdownProps) => {
         }
     };
 
-    const setActiveChanged = (active: boolean) => {
-        setActive(active);
-        props.activeChanged?.(active);
+    const setActiveChanged = (isActive: boolean) => {
+        setActive(isActive);
+        props.activeChanged?.(isActive);
     };
 
     const dropdown: JSX.Element = (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 7c36a82f2..e806675aa 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -688,7 +688,7 @@ pie title Minerals in my tap water
             { title: "Center",    icon: "align-center",  toolTip: "Center Align Stack",  btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"hcenter",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
-    static cardTools(): Button[] {
+    static sortTools(): Button[] {
         return [
             { title: "Time",      icon:"hourglass-half", toolTip:"Sort by most recent document creation",          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"time",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Type",      icon:"eye",            toolTip:"Sort by document type",                          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
@@ -704,7 +704,7 @@ pie title Minerals in my tap water
          ]
     }
   
-    static tagGroupTools(): Button[] {
+    static filterTools(): Button[] {
         const defaultTagButtonDescs = [
             { title: "Star", isSystem: false,icon: "star",  toolTip:"Click to toggle visibility of Star tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#star", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
             { title: "Like", isSystem: false,icon: "heart", toolTip:"Click to toggle visibility of Like tagged Docs",  btnType: ButtonType.ToggleButton,  expertMode: false, toolType:"#like", funcs: {}, scripts: { onClick: '{ return setTagFilter(this.toolType, _added_, _readOnly_);}'}},
@@ -723,7 +723,6 @@ pie title Minerals in my tap water
             { title: "Grid",      icon: "border-all",          toolTip: "Show Grid",           btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid",       funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
             { title: "Fit All",   icon: "object-group",        toolTip: "Fit Docs to View (double click to make sticky)",btnType: ButtonType.ToggleButton, ignoreClick:true, expertMode: false, toolType:"viewAll",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}', onDoubleClick: '{ return showFreeform(this.toolType, _readOnly_, true);}'}}, // Only when floating document is selected in freeform
             { title: "Clusters",  icon: "braille",             toolTip: "Show Doc Clusters",   btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters",   funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
-            { title: "Cards",     icon: "brain",               toolTip: "Flashcards",          btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
         ]
     }
     static textTools():Button[] {
@@ -822,15 +821,14 @@ pie title Minerals in my tap water
             { title: "Num",     icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton,          expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         expertMode: true,  toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'},                  width: 30,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             
-            { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
+            { title: "Filter",  icon: "=",      toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.filterTools(), ignoreClick:true,  toolType:DocumentType.COL,            funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
+            { title: "Sort",    icon: "Sort",   toolTip: "Sort Documents",       subMenu: CurrentUserUtils.sortTools(),   expertMode: false, toolType:DocumentType.COL,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Text",    icon: "Text",   toolTip: "Text functions",       subMenu: CurrentUserUtils.textTools(),   expertMode: false, toolType:DocumentType.RTF,            funcs: {                                                                             linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Ink",     icon: "Ink",    toolTip: "Ink functions",        subMenu: CurrentUserUtils.inkTools(),    expertMode: false, toolType:DocumentType.INK,            funcs: {hidden: `IsExploreMode()`,                                                   linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
             { title: "Doc",     icon: "Doc",    toolTip: "Freeform Doc tools",   subMenu: CurrentUserUtils.freeTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "View",    icon: "View",   toolTip: "View tools",           subMenu: CurrentUserUtils.viewTools(),   expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Stack",   icon: "View",   toolTip: "Stacking tools",       subMenu: CurrentUserUtils.stackTools(),  expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             
-            { title: "Card",    icon: "Card",   toolTip: "Card View Tools",      subMenu: CurrentUserUtils.cardTools(),   expertMode: false, toolType:CollectionViewType.Card,     funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
-         // { title: "Create",  icon: "Create", toolTip: "Assign card labels",   subMenu: CurrentUserUtils.labelTools(),  expertMode: false, toolType:CollectionViewType.Card,     funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
             { title: "Web",     icon: "Web",    toolTip: "Web functions",        subMenu: CurrentUserUtils.webTools(),    expertMode: false, toolType:DocumentType.WEB,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when Web is selected
             { title: "Video",   icon: "Video",  toolTip: "Video functions",      subMenu: CurrentUserUtils.videoTools(),  expertMode: false, toolType:DocumentType.VID,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
             { title: "Image",   icon: "Image",  toolTip: "Image functions",      subMenu: CurrentUserUtils.imageTools(),  expertMode: false, toolType:DocumentType.IMG,            funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 1bca68846..aaf33c3a1 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,16 +1,16 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import * as CSS from 'csstype';
-import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { URLField } from '../../../fields/URLField';
 import { gptImageLabel } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -25,21 +25,12 @@ import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
+import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { SettingsManager } from '../../util/SettingsManager';
 
-enum cardSortings {
-    Time = 'time',
-    Type = 'type',
-    Color = 'color',
-    Chat = 'chat',
-    Tag = 'tag',
-    None = '',
-}
-
 /**
  * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
  * sort and filter using presets, and customize your experience with chat gpt.
@@ -60,7 +51,6 @@ export class CollectionCardView extends CollectionSubView() {
     @observable _forceChildXf = 0;
     @observable _hoveredNodeIndex = -1;
     @observable _docRefs = new ObservableMap<Doc, DocumentView>();
-    @observable _docDraggedIndex: number = -1;
     @observable _cursor: CSS.Property.Cursor = 'ew-resize';
 
     constructor(props: SubCollectionViewProps) {
@@ -102,18 +92,8 @@ export class CollectionCardView extends CollectionSubView() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        this._disposers.sort = reaction(
-            () => GPTPopup.Instance.visible,
-            isVis => {
-                if (isVis) {
-                    this.openChatPopup();
-                } else {
-                    this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
-                }
-            }
-        );
         // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
-        // when inquired from the dom (below in childScreenToLocal).  When the doc is actually renders, we need to act like the
+        // when inquired from the dom (below in childScreenToLocal).  When the doc is actually rendered, we need to act like the
         // dash data just changed and trigger a React involidation with the correct data (read from the dom).
         this._disposers.child = reaction(
             () => [this.Document.x, this.Document.y],
@@ -136,14 +116,11 @@ export class CollectionCardView extends CollectionSubView() {
         this._dropDisposer?.();
     }
 
-    @computed get cardSort() {
-        return StrCast(this.Document.card_sort) as cardSortings;
-    }
     /**
      * Number of rows of cards to be rendered
      */
     @computed get numRows() {
-        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+        return Math.ceil(this.childDocs.length / this._maxRowCount);
     }
     /**
      * Circle arc size, in radians, to layout cards
@@ -211,7 +188,7 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns the card's new index
      */
     findCardDropIndex = (mouseX: number, mouseY: number) => {
-        const cardCount = this.sortedDocs.length;
+        const cardCount = this.childDocs.length;
         let index = 0;
         const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
 
@@ -245,8 +222,8 @@ export class CollectionCardView extends CollectionSubView() {
      */
     @action
     onPointerMove = (x: number, y: number) => {
-        if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
-            this._docDraggedIndex = this.findCardDropIndex(x, y);
+        if (DragManager.docsBeingDragged.some(doc => this.childDocs.includes(doc)) || SnappingManager.CanEmbed) {
+            this.docDraggedIndex = this.findCardDropIndex(x, y);
         }
     };
 
@@ -259,11 +236,11 @@ export class CollectionCardView extends CollectionSubView() {
     onInternalDrop = undoable(
         action((e: Event, de: DragManager.DropEvent) => {
             if (de.complete.docDragData) {
-                const dragIndex = this._docDraggedIndex;
+                const dragIndex = this.docDraggedIndex;
                 const draggedDoc = DragManager.docsBeingDragged[0];
                 if (dragIndex > -1 && draggedDoc) {
-                    this._docDraggedIndex = -1;
-                    const sorted = this.sortedDocs;
+                    this.docDraggedIndex = -1;
+                    const sorted = this.childDocs;
                     const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
 
                     this.Document.card_sort = '';
@@ -295,50 +272,6 @@ export class CollectionCardView extends CollectionSubView() {
             .map(({ i }) => i)
             .join('.');
 
-    /**
-     * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
-     * front, latter cards to the back
-     * @param docs
-     * @param sortType
-     * @param isDesc
-     * @returns
-     */
-    sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
-        const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
-        sortType &&
-            docs.sort((docA, docB) => {
-                const [typeA, typeB] = (() => {
-                    switch (sortType) {
-                        default:                
-                        case cardSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
-                        case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
-                        case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
-                        case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
-                        case cardSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
-                    }
-                })(); //prettier-ignore
-                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
-            });
-        if (dragIndex !== -1) {
-            const draggedDoc = DragManager.docsBeingDragged[0];
-            const originalIndex = docs.findIndex(doc => doc === draggedDoc);
-
-            originalIndex !== -1 && docs.splice(originalIndex, 1);
-            draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
-        }
-
-        return docs;
-    };
-
-    @computed get sortedDocs() {
-        return this.sort(
-            this.childCards.map(card => card.layout),
-            this.cardSort,
-            BoolCast(this.Document.card_sort_isDesc),
-            this._docDraggedIndex
-        );
-    }
-
     isChildContentActive = computedFn(
         (doc: Doc) => () =>
             this._props.isContentActive?.() === false
@@ -560,16 +493,6 @@ export class CollectionCardView extends CollectionSubView() {
         });
     }, '');
 
-    /**
-     * Opens up the chat popup and starts the process for smart sorting.
-     */
-    openChatPopup = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setMode(GPTPopupMode.CARD);
-        GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
-        await this.childPairStringListAndUpdateSortDesc();
-    };
-
     childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
@@ -615,7 +538,7 @@ export class CollectionCardView extends CollectionSubView() {
         setupMoveUpEvents(
             this,
             e,
-            (e: PointerEvent, down: number[], delta: number[]) => {
+            (emove: PointerEvent, down: number[], delta: number[]) => {
                 this.layoutDoc._cardWidth = Math.max(10, delta[0] < 0 ? Math.floor(this.cardWidth + delta[0]) : Math.ceil(this.cardWidth + delta[0]));
                 return false;
             },
@@ -662,9 +585,9 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        trace();
+        console.log(this.docDraggedIndex, this.childDocs[0].title, this.childDocs[1].title);
         // Map sorted documents to their rendered components
-        return this.sortedDocs.map((doc, index) => {
+        return this.childDocs.map((doc, index) => {
             const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
 
             const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
@@ -674,7 +597,7 @@ export class CollectionCardView extends CollectionSubView() {
             const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
             const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), 
                                     (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
-            const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
+            const hscale = Math.min(this.childDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
             return (
                 <div
                     key={doc[Id]}
@@ -710,17 +633,13 @@ export class CollectionCardView extends CollectionSubView() {
     curDoc = () => DocCast(this.layoutDoc._card_curDoc);
 
     render() {
-        trace();
         const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
                 ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
-                onPointerDown={action(e => {
-                    if (e.button === 2 || e.ctrlKey) return;
-                    this.releaseCurDoc();
-                })}
-                onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+                onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()}
+                onPointerLeave={action(() => (this.docDraggedIndex = -1))}
                 onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
                 onDrop={this.onExternalDrop.bind(this)}
                 style={{
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 0c059f729..5e99bec39 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, makeObservable, observable } from 'mobx';
 import * as React from 'react';
 import * as rp from 'request-promise';
-import { ClientUtils, returnFalse } from '../../../ClientUtils';
+import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils';
 import CursorField from '../../../fields/CursorField';
 import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc';
 import { AclPrivate, DocData } from '../../../fields/DocSymbols';
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
+import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
 import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -29,6 +29,14 @@ import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
 
+export enum docSortings {
+    Time = 'time',
+    Type = 'type',
+    Color = 'color',
+    Chat = 'chat',
+    Tag = 'tag',
+    None = '',
+}
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
     isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -150,6 +158,8 @@ export function CollectionSubView<X>() {
         unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])];
         childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
         searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs);
+
+        @observable docDraggedIndex = -1;
         @computed.struct get childDocs() {
             TraceMobx();
             let rawdocs: (Doc | Promise<Doc>)[] = [];
@@ -166,8 +176,10 @@ export function CollectionSubView<X>() {
                 const templateRoot = this._props.TemplateDataDocument;
                 rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : [];
             }
-
-            const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc);
+            const childDocs = this.childSortedDocs(
+                rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc),
+                this.docDraggedIndex
+            );
 
             const childDocFilters = this.childDocFilters();
             const childFiltersByRanges = this.childDocRangeFilters();
@@ -214,6 +226,35 @@ export function CollectionSubView<X>() {
             return docsforFilter;
         }
 
+        childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
+            const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
+            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+            const docs = docsIn.slice();
+            if (sortType) {
+                docs.sort((docA, docB) => {
+                    const [typeA, typeB] = (() => {
+                        switch (sortType) {
+                            default:                
+                            case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
+                            case docSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
+                            case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
+                            case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+                            case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
+                        }
+                    })(); //prettier-ignore
+                    return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
+                });
+            }
+            if (dragIndex !== -1) {
+                const draggedDoc = DragManager.docsBeingDragged[0];
+                const originalIndex = docs.findIndex(doc => doc === draggedDoc);
+
+                originalIndex !== -1 && docs.splice(originalIndex, 1);
+                draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
+            }
+            return docs;
+        };
+
         @action
         protected async setCursorPosition(position: [number, number]) {
             let ind;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 77f1db9ad..c071c5fb8 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -58,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
 
     @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
-    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+    @computed get practiceMode() {  return this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
 
     btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
     btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -179,7 +179,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
             </div>
         );
     }
-    tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
+    tryFilterOut = (doc: Doc) => (this.practiceMode && doc?._layout_flashcardType && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
     render() {
         return (
             <div className="FlashcardPracticeUI">
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 542417531..b02eabab0 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -40,6 +40,7 @@ import { WebBox } from '../nodes/WebBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import { OpenWhere } from '../nodes/OpenWhere';
+import { docSortings } from '../collections/CollectionSubView';
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function IsNoneSelected() {
@@ -197,43 +198,35 @@ ScriptingGlobals.add(function showFreeform(
             checkResult: (doc: Doc) => BoolCast(doc?._freeform_useClusters, false),
             setDoc: (doc: Doc) => { doc._freeform_useClusters = !doc._freeform_useClusters; },
         }],
-        ['flashcards', {
-            checkResult: (doc: Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
-            setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
-        }],
         ['time', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "time",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "time" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Time}, // prettier-ignore
         }],
         ['docType', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "type",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "type" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Type}, // prettier-ignore
         }],
         ['color', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "color",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc?.[Doc.LayoutFieldKey(doc)+"_sort"] === "color" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Color}, // prettier-ignore
         }],
         ['tag', {
-            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
         }],
         ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
-            setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.card_sort_isDesc = false;
-            },
+            checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
         }],
         ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
-            setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.card_sort_isDesc = true;
-            },
+            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => GPTPopup.Instance.visible,
             setDoc: (doc: Doc, dv: DocumentView) => {
                 if (GPTPopup.Instance.visible){
-                    doc.card_sort = ''
+                    doc[Doc.LayoutFieldKey(doc)+"_sort"] = '';
                     GPTPopup.Instance.setVisible(false);
    
                 } else {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index e0c360132..f5291a4c1 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,7 +1,7 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@mui/material';
 import axios from 'axios';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import ReactLoading from 'react-loading';
diff --git a/src/client/views/nodes/imageEditor/ImageEditor.tsx b/src/client/views/nodes/imageEditor/ImageEditor.tsx
index 2ae6ee1dd..6b1d05031 100644
--- a/src/client/views/nodes/imageEditor/ImageEditor.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditor.tsx
@@ -551,8 +551,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         img.src = src;
         if (!currImg.current || !originalImg.current || !imageRootDoc) return undefined;
         try {
-            const res = await createNewImgDoc(img, false);
-            return res;
+            return await createNewImgDoc(img, false);
         } catch (err) {
             console.log(err);
         }
@@ -589,9 +588,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 // disable once edited has been clicked (doesn't make sense to change after first edit)
                                 disabled={edited}
                                 checked={isNewCollection}
-                                onChange={() => {
-                                    setIsNewCollection(prev => !prev);
-                                }}
+                                onChange={() => setIsNewCollection(prev => !prev)}
                             />
                         }
                         label="Create New Collection"
@@ -610,49 +607,13 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
         return (
             <div className="sideControlsContainer" style={{ backgroundColor: bgColor }}>
                 <div className="sideControls">
-                    <div className="imageToolsContainer">
-                        {imageEditTools.map(tool => {
-                            return ImageToolButton(tool, tool.type === currTool.type, changeTool);
-                        })}
-                    </div>
+                    <div className="imageToolsContainer">{imageEditTools.map(tool => ImageToolButton(tool, tool.type === currTool.type, changeTool))}</div>
                     {currTool.type == ImageToolType.Cut && (
                         <div className="cutToolsContainer">
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Keep in"
-                                type={Type.TERT}
-                                color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.IN);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Keep out"
-                                type={Type.TERT}
-                                color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.OUT);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Draw in"
-                                type={Type.TERT}
-                                color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.DRAW_IN);
-                                }}
-                            />
-                            <Button
-                                style={{ width: '100%' }}
-                                text="Erase"
-                                type={Type.TERT}
-                                color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor}
-                                onClick={() => {
-                                    setCutType(CutMode.ERASE);
-                                }}
-                            />
+                            <Button style={{ width: '100%' }} text="Keep in" type={Type.TERT} color={cutType == CutMode.IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.IN)} />
+                            <Button style={{ width: '100%' }} text="Keep out" type={Type.TERT} color={cutType == CutMode.OUT ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.OUT)} />
+                            <Button style={{ width: '100%' }} text="Draw in" type={Type.TERT} color={cutType == CutMode.DRAW_IN ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.DRAW_IN)} />
+                            <Button style={{ width: '100%' }} text="Erase" type={Type.TERT} color={cutType == CutMode.ERASE ? SettingsManager.userColor : bgColor} onClick={() => setCutType(CutMode.ERASE)} />
                         </div>
                     )}
                     <div className="sliderContainer" onPointerDown={e => e.stopPropagation()}>
@@ -669,9 +630,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 defaultValue={genFillTool.sliderDefault}
                                 size="small"
                                 valueLabelDisplay="auto"
-                                onChange={(e, val) => {
-                                    setCursorData(prev => ({ ...prev, width: val as number }));
-                                }}
+                                onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
                         {currTool.type === ImageToolType.Cut && (
@@ -687,9 +646,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 defaultValue={cutTool.sliderDefault}
                                 size="small"
                                 valueLabelDisplay="auto"
-                                onChange={(e, val) => {
-                                    setCursorData(prev => ({ ...prev, width: val as number }));
-                                }}
+                                onChange={(e, val) => setCursorData(prev => ({ ...prev, width: val as number }))}
                             />
                         )}
                     </div>
@@ -701,9 +658,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 e.stopPropagation();
                                 handleUndo();
                             }}
-                            onPointerUp={e => {
-                                e.stopPropagation();
-                            }}
+                            onPointerUp={e => e.stopPropagation()}
                             color={activeColor}
                             tooltip="Undo"
                             icon={<IoMdUndo />}
@@ -714,9 +669,7 @@ const ImageEditor = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc
                                 e.stopPropagation();
                                 handleRedo();
                             }}
-                            onPointerUp={e => {
-                                e.stopPropagation();
-                            }}
+                            onPointerUp={e => e.stopPropagation()}
                             color={activeColor}
                             tooltip="Redo"
                             icon={<IoMdRedo />}
diff --git a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
index de2116253..e810881a5 100644
--- a/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
+++ b/src/client/views/nodes/imageEditor/ImageEditorButtons.tsx
@@ -55,6 +55,7 @@ export function ImageToolButton(tool: ImageEditTool, isActive: boolean, selectTo
     return (
         <div className="imageEditorButtonContainer">
             <Button
+                key={tool.name}
                 style={{ width: '100%' }}
                 text={tool.name}
                 type={Type.TERT}
-- 
cgit v1.2.3-70-g09d2


From c079b7aa916d7eb35ce186806ca312c84e910cb2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 14 Feb 2025 14:07:18 -0500
Subject: major restructure of GPTPopup so that it works with all collections.

---
 src/client/apis/gpt/GPT.ts                         |  28 ++-
 src/client/views/ViewBoxInterface.ts               |   1 +
 .../views/collections/CollectionCardDeckView.tsx   | 157 ++------------
 src/client/views/collections/CollectionSubView.tsx |   1 +
 src/client/views/collections/CollectionView.tsx    |   4 +-
 .../nodes/chatbot/tools/CreateDocumentTool.ts      |  38 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 231 +++++++++++++--------
 src/fields/Doc.ts                                  |  24 ++-
 src/fields/Types.ts                                |   4 +
 10 files changed, 240 insertions(+), 252 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index dc969da16..e1ae99d97 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -8,6 +8,10 @@ export enum GPTTypeStyle {
     GeneralInfo = 4,
     SortDocs = 5,
 }
+
+export const DescriptionSeperator = '======';
+export const DocSeperator = '------';
+
 enum GPTCallType {
     SUMMARY = 'summary',
     COMPLETION = 'completion',
@@ -41,7 +45,7 @@ type GPTCallOpts = {
     prompt: string;
 };
 
-const callTypeMap: { [type: string]: GPTCallOpts } = {
+const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
     // newest model: gpt-4
     summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize the text given in simpler terms.' },
     edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword the text.' },
@@ -69,7 +73,13 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.25,
-        prompt: "The user is going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Sort them by the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and NO commas",
+        prompt: `The user is going to give you a list of descriptions. 
+                Each one is separated by '${DescriptionSeperator}' on either side. 
+                Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. 
+                Sort them by the user's specifications. 
+                Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. 
+                Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning for the way you sorted (and mention nothing about the formatting details given in this description). 
+                It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
     },
     describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
     flashcard: {
@@ -145,20 +155,28 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
-        prompt: "I'm going to give you a list of descriptions. Each one is separated by `======` on either side. Descriptions will vary in length, so make sure to only separate when you see `======`. Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications. Make sure each description is only in the list once. Each item should be separated by `======`. Immediately afterward, surrounded by `------` on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description). It is VERY important that you format it exactly as described, ensuring the proper number of `=` and `-` (6 of each) and no commas",
+        prompt: `I'm going to give you a list of descriptions.
+                Each one is separated by '${DescriptionSeperator}' on either side. 
+                Descriptions will vary in length, so make sure to only separate when you see '${DescriptionSeperator}'. 
+                Based on the question the user asks, provide a subset of the given descriptions that best matches the user's specifications. 
+                Make sure each description is only in the list once. Each item should be separated by '${DescriptionSeperator}'. 
+                Immediately afterward, surrounded by '${DocSeperator}' on BOTH SIDES, provide some insight into your reasoning in the 2nd person (and mention nothing about the formatting details given in this description). 
+                It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
     },
 
     info: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
-        prompt: "Answer the user's question with a short (<100 word) response. If a particular document is selected I will provide that information (which may help with your response)",
+        prompt: `Answer the user's question with a short (<100 word) response. 
+                 If a particular document is selected I will provide that information (which may help with your response)`,
     },
     rubric: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
-        prompt: "BRIEFLY (<25 words) provide a definition for the following term. It will be used as a rubric to evaluate the user's understanding of the topic",
+        prompt: `BRIEFLY (<25 words) provide a definition for the following term. 
+                 It will be used as a rubric to evaluate the user's understanding of the topic`,
     },
 };
 let lastCall = '';
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index a66a20cf6..30da8c616 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -22,6 +22,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
         return ''; //
     }
     promoteCollection?: () => void; // moves contents of collection to parent
+    hasChildDocs?: () => Doc[];
     updateIcon?: (usePanelDimensions?: boolean) => Promise<void>; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
     restoreView?: (viewSpec: Doc) => boolean; // DEPRECATED: do not use, it will go away.  see PresBox.restoreTargetDocView
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index e00aa65d7..d7f4251f3 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,22 +1,22 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as CSS from 'csstype';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
-import * as CSS from 'csstype';
-import { ClientUtils, imageUrlToBase64, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { ClientUtils, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Animation, DocData } from '../../../fields/DocSymbols';
+import { Animation } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { URLField } from '../../../fields/URLField';
-import { gptImageLabel, GPTTypeStyle } from '../../apis/gpt/GPT';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
+import { SettingsManager } from '../../util/SettingsManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoable, UndoManager } from '../../util/UndoManager';
@@ -25,11 +25,8 @@ import { StyleProp } from '../StyleProp';
 import { TagItem } from '../TagsView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FocusViewOptions } from '../nodes/FocusViewOptions';
-import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
-import { CollectionSubView, docSortings, SubCollectionViewProps } from './CollectionSubView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { SettingsManager } from '../../util/SettingsManager';
+import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 
 /**
  * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
@@ -42,7 +39,6 @@ import { SettingsManager } from '../../util/SettingsManager';
 export class CollectionCardView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [key: string]: IReactionDisposer } = {};
-    private _textToDoc = new Map<string, Doc>();
     private _oldWheel: HTMLElement | null = null;
     private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
     private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
@@ -74,22 +70,7 @@ export class CollectionCardView extends CollectionSubView() {
         return Math.ceil(this.cardDeckWidth / this.cardWidth);
     }
 
-    /**
-     * update's gpt's doc-text list and initialize callbacks to respond to GPT output
-     */
-    updateDocumentDescriptions = () =>
-        this.childPairStringList().then(docDescriptions => {
-            GPTPopup.Instance.setDocumentDescriptions(docDescriptions.join());
-            GPTPopup.Instance.onGptResponse = this.processGptResponse;
-            GPTPopup.Instance.onQuizRandom = this.randomlyChooseDoc;
-        });
-
     componentDidMount() {
-        this._disposers.chatVis = reaction(
-            () => GPTPopup.Instance.Visible,
-            vis => !vis && this.onGptHide()
-        );
-        GPTPopup.Instance.setRetrieveDocDescriptionsCallback(this.Document, this.updateDocumentDescriptions);
         this._props.setContentViewBox?.(this);
         // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
         // when inquired from the dom (below in childScreenToLocal).  When the doc is actually rendered, we need to act like the
@@ -110,10 +91,7 @@ export class CollectionCardView extends CollectionSubView() {
         );
     }
 
-    onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove');
     componentWillUnmount() {
-        GPTPopup.Instance.onGptResponse = undefined;
-        GPTPopup.Instance.onQuizRandom = undefined;
         Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
         this._dropDisposer?.();
     }
@@ -128,7 +106,7 @@ export class CollectionCardView extends CollectionSubView() {
      * Circle arc size, in radians, to layout cards
      */
     @computed get archAngle() {
-        return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childCards.length < this._maxRowCount ? this.childCards.length / this._maxRowCount : 1);
+        return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childDocsNoInk.length < this._maxRowCount ? this.childDocsNoInk.length / this._maxRowCount : 1);
     }
     /**
      * Spacing card rows as a percent of Doc size.   100 means rows spread out to fill 100% of the Doc vertically.  Default is 60%
@@ -140,7 +118,7 @@ export class CollectionCardView extends CollectionSubView() {
     /**
      * The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's)
      */
-    @computed get childCards() {
+    @computed get childDocsNoInk() {
         return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
     }
 
@@ -148,7 +126,7 @@ export class CollectionCardView extends CollectionSubView() {
      * how much to scale down the contents of the view so that everything will fit
      */
     @computed get fitContentScale() {
-        const length = Math.min(this.childCards.length, this._maxRowCount);
+        const length = Math.min(this.childDocsNoInk.length, this._maxRowCount);
         return (this.childPanelWidth() * length) / this._props.PanelWidth();
     }
 
@@ -164,17 +142,12 @@ export class CollectionCardView extends CollectionSubView() {
         return this._props.PanelWidth() - 2 * this.xMargin;
     }
 
-    /**
-     * When in quiz mode, randomly selects a document
-     */
-    randomlyChooseDoc = () => (this.layoutDoc._card_curDoc = this.childDocs[Math.floor(Math.random() * this.childDocs.length)]);
-
     setHoveredNodeIndex = action((index: number) => {
         if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
     });
 
     isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
-    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
+    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childDocsNoInk.length > this._maxRowCount ? this._maxRowCount : this.childDocsNoInk.length) / this.nativeScaling));
     childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
@@ -318,10 +291,10 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns number of cards in row that contains index
      */
     cardsInRowThatIncludesCardIndex = (index: number) => {
-        if (this.childCards.length < this._maxRowCount) {
-            return this.childCards.length;
+        if (this.childDocsNoInk.length < this._maxRowCount) {
+            return this.childDocsNoInk.length;
         }
-        const totalCards = this.childCards.length;
+        const totalCards = this.childDocsNoInk.length;
         if (index < totalCards - (totalCards % this._maxRowCount)) {
             return this._maxRowCount;
         }
@@ -385,102 +358,6 @@ export class CollectionCardView extends CollectionSubView() {
             : this.translateY(index);
     };
 
-    /**
-     * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
-     * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
-     * inputted into the gpt prompt to sort everything together
-     * @returns
-     */
-    childPairStringList = () => {
-        const docToText = (doc: Doc) => {
-            switch (doc.type) {
-                case DocumentType.PDF:  return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
-                case DocumentType.IMG:  return this.getImageDesc(doc);
-                case DocumentType.RTF:  return StrCast(RTFCast(doc.text).Text);
-                default:                return StrCast(doc.title);
-            } // prettier-ignore
-        };
-        const docTextPromises = this.childCards
-            .map(pair => pair.layout)
-            .map(async doc => {
-                const docText = (await docToText(doc)) ?? '';
-                doc.gptInputText = docText;
-                this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
-                return `======${docText.replace(/\n/g, ' ').trim()}======`;
-            });
-        return Promise.all<string>(docTextPromises);
-    };
-
-    /**
-     * Calls the gpt API to generate descriptions for the images in the view
-     * @param image
-     * @returns
-     */
-    getImageDesc = async (image: Doc) => {
-        if (StrCast(image.description)) return StrCast(image.description); // Return existing description
-        const { href } = (image.data as URLField).url;
-        const hrefParts = href.split('.');
-        const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
-        try {
-            const hrefBase64 = await imageUrlToBase64(hrefComplete);
-            const response = await gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.');
-            image[DocData].description = response.trim();
-            return response; // Return the response from gptImageLabel
-        } catch (error) {
-            console.log(error);
-        }
-        return '';
-    };
-
-    /**
-     * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
-     * usable code
-     * @param gptOutput
-     * @param questionType
-     * @param tag
-     */
-    processGptResponse = (gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
-        undoable(() => {
-            // Split the string into individual list items
-            const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
-            if (questionType === GPTTypeStyle.Filter) {
-                this.childDocs.forEach(d => {
-                    TagItem.removeTagFromDoc(d, '#chat');
-                });
-            }
-
-            if (questionType === GPTTypeStyle.SortDocs) {
-                this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
-            }
-
-            listItems.forEach((item, index) => {
-                const normalizedItem = item.trim();
-                // find the corresponding Doc in the textToDoc map
-                const doc = this._textToDoc.get(normalizedItem);
-                if (doc) {
-                    switch (questionType) {
-                        case GPTTypeStyle.SortDocs:
-                            doc.chatIndex = index;
-                            break;
-                        case GPTTypeStyle.AssignTags:
-                            if (tag) {
-                                const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
-                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
-                                TagItem.addTagToDoc(doc, filterTag);
-                            }
-                            break;
-                        case GPTTypeStyle.Filter:
-                            TagItem.addTagToDoc(doc, '#chat');
-                            Doc.setDocFilter(this.Document, 'tags', '#chat', 'check');
-                            break;
-                    }
-                } else {
-                    console.warn(`No matching document found for item: ${normalizedItem}`);
-                }
-            });
-        }, '')();
-
     childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
@@ -620,7 +497,7 @@ export class CollectionCardView extends CollectionSubView() {
     curDoc = () => DocCast(this.layoutDoc._card_curDoc);
 
     render() {
-        const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
+        const fitContentScale = this.childDocsNoInk.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
@@ -652,7 +529,7 @@ export class CollectionCardView extends CollectionSubView() {
                     <div
                         className="collectionCardView-flashcardUI"
                         style={{
-                            pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+                            pointerEvents: this.childDocsNoInk.length === 0 ? undefined : 'none',
                             height: `${100 / this.nativeScaling / fitContentScale}%`,
                             width: `${100 / this.nativeScaling / fitContentScale}%`,
                             transform: `scale(${this.nativeScaling * fitContentScale})`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5e99bec39..954f9aa4c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -121,6 +121,7 @@ export function CollectionSubView<X>() {
             return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout.  hopefully nothing depending on the previous code.
         }
 
+        hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout);
         @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] {
             const { Document, TemplateDataDocument } = this._props;
             const validPairs = this.childDocs
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 6f0833a22..a4900e9d7 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -89,6 +89,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         TraceMobx();
         if (type === undefined) return null;
         switch (type) {
+            default:
+            case CollectionViewType.Freeform:     return <CollectionFreeFormView    key="collview" {...props} />;
             case CollectionViewType.Schema:       return <CollectionSchemaView      key="collview" {...props} />;
             case CollectionViewType.Calendar:     return <CalendarBox               key="collview" {...props} />;
             case CollectionViewType.Docking:      return <CollectionDockingView     key="collview" {...props} />;
@@ -105,8 +107,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
             case CollectionViewType.Time:         return <CollectionTimeView        key="collview" {...props} />;
             case CollectionViewType.Grid:         return <CollectionGridView        key="collview" {...props} />;
             case CollectionViewType.Card:         return <CollectionCardView        key="collview" {...props} />;
-            case CollectionViewType.Freeform:     
-            default:                              return <CollectionFreeFormView    key="collview" {...props} />;
         }
     };
 
diff --git a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
index 6dc36b0d1..284879a4a 100644
--- a/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateDocumentTool.ts
@@ -263,79 +263,79 @@ const standardOptions = ['title', 'backgroundColor'];
  * Description of document options and data field for each type.
  */
 const documentTypesInfo: { [key in supportedDocTypes]: { options: string[]; dataDescription: string } } = {
-    [supportedDocTypes.comparison]: {
+    comparison: {
         options: [...standardOptions, 'fontColor', 'text_align'],
         dataDescription: 'an array of two documents of any kind that can be compared.',
     },
-    [supportedDocTypes.deck]: {
+    deck: {
         options: [...standardOptions, 'fontColor', 'text_align'],
         dataDescription: 'an array of flashcard docs',
     },
-    [supportedDocTypes.flashcard]: {
+    flashcard: {
         options: [...standardOptions, 'fontColor', 'text_align'],
         dataDescription: 'an array of two strings.  the first string contains a question, and the second string contains an answer',
     },
-    [supportedDocTypes.text]: {
+    text: {
         options: [...standardOptions, 'fontColor', 'text_align'],
         dataDescription: 'The text content of the document.',
     },
-    [supportedDocTypes.web]: {
+    web: {
         options: [],
         dataDescription: 'A URL to a webpage. Example: https://en.wikipedia.org/wiki/Brown_University',
     },
-    [supportedDocTypes.html]: {
+    html: {
         options: [],
         dataDescription: 'The HTML-formatted text content of the document.',
     },
-    [supportedDocTypes.equation]: {
+    equation: {
         options: [...standardOptions, 'fontColor'],
         dataDescription: 'The equation content represented as a MathML string.',
     },
-    [supportedDocTypes.functionplot]: {
+    functionplot: {
         options: [...standardOptions, 'function_definition'],
         dataDescription: 'The function definition(s) for plotting. Provide as a string or array of function definitions.',
     },
-    [supportedDocTypes.dataviz]: {
+    dataviz: {
         options: [...standardOptions, 'chartType'],
         dataDescription: 'A string of comma-separated values representing the CSV data.',
     },
-    [supportedDocTypes.notetaking]: {
+    notetaking: {
         options: standardOptions,
         dataDescription: 'An array of related text documents with small amounts of text.',
     },
-    [supportedDocTypes.rtf]: {
+    rtf: {
         options: standardOptions,
         dataDescription: 'The rich text content in RTF format.',
     },
-    [supportedDocTypes.image]: {
+    image: {
         options: standardOptions,
         dataDescription: `A url string that must end with '.png', '.jpeg', '.gif', or '.jpg'`,
     },
-    [supportedDocTypes.pdf]: {
+    pdf: {
         options: standardOptions,
         dataDescription: 'the pdf content as a PDF file url.',
     },
-    [supportedDocTypes.audio]: {
+    audio: {
         options: standardOptions,
         dataDescription: 'The audio content as a file url.',
     },
-    [supportedDocTypes.video]: {
+    video: {
         options: standardOptions,
         dataDescription: 'The video content as a file url.',
     },
-    [supportedDocTypes.message]: {
+    message: {
         options: standardOptions,
         dataDescription: 'The message content of the document.',
     },
-    [supportedDocTypes.diagram]: {
+    diagram: {
         options: standardOptions,
         dataDescription: 'diagram content as a text string in Mermaid format.',
     },
-    [supportedDocTypes.script]: {
+    script: {
         options: standardOptions,
         dataDescription: 'The compilable JavaScript code. Use this for creating scripts.',
     },
-    [supportedDocTypes.collection]: {
+    collection: {
         options: [...standardOptions, 'type_collection'],
         dataDescription: 'A collection of Docs represented as an array.',
     },
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2e14fb1d9..6960247e9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1063,9 +1063,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     generateImage = async () => {
         GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
-        GPTPopup.Instance?.setImgTargetDoc(this.Document);
-        GPTPopup.Instance.addToCollection = this._props.addDocument;
-        GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text);
+        GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text, this.Document, this._props.addDocument);
     };
 
     breakupDictation = () => {
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 0b1ee78e3..f09d786d0 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,6 +1,6 @@
 import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
@@ -10,12 +10,15 @@ import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
-import { GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DescriptionSeperator, DocSeperator, GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs } from '../../../documents/Documents';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { SnappingManager } from '../../../util/SnappingManager';
+import { undoable } from '../../../util/UndoManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
+import { TagItem } from '../../TagsView';
+import { docSortings } from '../../collections/CollectionSubView';
 import { DocumentView } from '../../nodes/DocumentView';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
@@ -29,61 +32,71 @@ export enum GPTPopupMode {
     QUIZ_RESPONSE, // user definitions or explanations to be evaluated by GPT
 }
 
-export enum GPTQuizType {
-    CURRENT = 0,
-    CHOOSE = 1,
-    MULTIPLE = 2,
-}
-
 @observer
 export class GPTPopup extends ObservableReactComponent<object> {
     // eslint-disable-next-line no-use-before-define
     static Instance: GPTPopup;
-    private _retrieveDocDescriptions: (() => Promise<void>) | null = null;
     private _messagesEndRef: React.RefObject<HTMLDivElement>;
     private _correlatedColumns: string[] = [];
     private _dataChatPrompt: string | undefined = undefined;
     private _imgTargetDoc: Doc | undefined;
     private _textAnchor: Doc | undefined;
     private _dataJson: string = '';
-    private _documentDescriptions: string = '';
+    private _documentDescriptions: Promise<string> | undefined; // a cache of the descriptions of all docs in the selected collection. makes it more efficient when asking GPT multiple questions about the collection.
     private _sidebarFieldKey: string = '';
     private _textToSummarize: string = '';
     private _imageDescription: string = '';
+    private _textToDocMap = new Map<string, Doc>(); // when GPT answers with a doc's content, this helps us find the Doc
+    private _addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+
+    constructor(props: object) {
+        super(props);
+        makeObservable(this);
+        GPTPopup.Instance = this;
+        this._messagesEndRef = React.createRef();
+    }
 
+    public addDoc: ((doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean) | undefined;
+    public createFilteredDoc: (axes?: string[]) => boolean = () => false;
     public setSidebarFieldKey = (id: string) => (this._sidebarFieldKey = id);
-    public setDocumentDescriptions = (t: string) => (this._documentDescriptions = t);
     public setImgTargetDoc = (anchor: Doc) => (this._imgTargetDoc = anchor);
     public setTextAnchor = (anchor: Doc) => (this._textAnchor = anchor);
-    public onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
-    public onQuizRandom?: () => void;
     public setDataJson = (text: string) => {
         if (text === '') this._dataChatPrompt = '';
         this._dataJson = text;
     };
 
-    constructor(props: object) {
-        super(props);
-        makeObservable(this);
-        GPTPopup.Instance = this;
-        this._messagesEndRef = React.createRef();
+    componentDidUpdate() {
+        this._gptProcessing && this.setStopAnimatingResponse(false);
+    }
+    componentDidMount(): void {
+        reaction(
+            () => DocumentView.Selected().lastElement(),
+            selDoc => {
+                const hasChildDocs = selDoc?.ComponentView?.hasChildDocs;
+                if (hasChildDocs) {
+                    this._textToDocMap.clear();
+                    this.setCollectionContext(selDoc.Document);
+                    this.onGptResponse = (sortResult: string, questionType: GPTTypeStyle, tag?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, tag);
+                    this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
+                    this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
+                            Doc.getDescription(doc).then(text => this._textToDocMap.set(text, doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+                        )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
+                }
+            },
+            { fireImmediately: true }
+        );
     }
-
-    componentDidUpdate = () => this._gptProcessing && this.setStopAnimatingResponse(false);
 
     @observable private _conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
     @observable private _chatEnabled: boolean = false;
     @action private setChatEnabled = (start: boolean) => (this._chatEnabled = start);
-    @observable public Visible: boolean = false;
-    @action public setVisible = (vis: boolean) => (this.Visible = vis);
     @observable private _gptProcessing: boolean = false;
-    @action public setGptProcessing = (loading: boolean) => (this._gptProcessing = loading);
+    @action private setGptProcessing = (loading: boolean) => (this._gptProcessing = loading);
     @observable private _responseText: string = '';
-    @action public setResponseText = (text: string) => (this._responseText = text);
+    @action private setResponseText = (text: string) => (this._responseText = text);
     @observable private _imgUrls: string[][] = [];
-    @action public setImgUrls = (imgs: string[][]) => (this._imgUrls = imgs);
-    @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY;
-    @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
+    @action private setImgUrls = (imgs: string[][]) => (this._imgUrls = imgs);
     @observable private _collectionContext: Doc | undefined = undefined;
     @action setCollectionContext = (doc: Doc | undefined) => (this._collectionContext = doc);
     @observable private _userPrompt: string = '';
@@ -91,23 +104,68 @@ export class GPTPopup extends ObservableReactComponent<object> {
     @observable private _quizAnswer: string = '';
     @action setQuizAnswer = (e: React.ChangeEvent<HTMLInputElement>) => (this._quizAnswer = e.target.value);
     @observable private _stopAnimatingResponse: boolean = false;
-    @action public setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done);
+    @action private setStopAnimatingResponse = (done: boolean) => (this._stopAnimatingResponse = done);
+
+    @observable private _mode: GPTPopupMode = GPTPopupMode.SUMMARY;
+    @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
+    @observable public Visible: boolean = false;
+    @action public setVisible = (vis: boolean) => (this.Visible = vis);
+
+    onQuizRandom?: () => void;
+    onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
+    questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
 
     /**
-     * sets callback needed to retrieve an updated description of the collection's documents
-     * @param collectionDoc - the collection doc context for the GPT prompts
-     * @param callback - function to retrieve document descriptions
+     * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
+     * usable code
+     * @param gptOutput
+     * @param questionType
+     * @param tag
      */
-    public setRetrieveDocDescriptionsCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
-        this.setCollectionContext(collectionDoc);
-        this._retrieveDocDescriptions = callback;
-    }
+    processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
+        undoable(() => {
+            // Split the string into individual list items
+            const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
 
-    public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
-    public createFilteredDoc: (axes?: string[]) => boolean = () => false;
-    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
-    public questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
+            if (questionType === GPTTypeStyle.Filter) {
+                docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, '#chat'));
+            }
+
+            if (questionType === GPTTypeStyle.SortDocs) {
+                docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
+            }
 
+            listItems.forEach((item, index) => {
+                const normalizedItem = item.replace(/\n/g, ' ').trim();
+                // find the corresponding Doc in the textToDoc map
+                const doc = textToDocMap.get(normalizedItem);
+                if (doc) {
+                    switch (questionType) {
+                        case GPTTypeStyle.SortDocs:
+                            doc.chatIndex = index;
+                            break;
+                        case GPTTypeStyle.AssignTags:
+                            if (tag) {
+                                const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
+                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
+                                TagItem.addTagToDoc(doc, filterTag);
+                            }
+                            break;
+                        case GPTTypeStyle.Filter:
+                            TagItem.addTagToDoc(doc, '#chat');
+                            Doc.setDocFilter(docView.Document, 'tags', '#chat', 'check');
+                            break;
+                    }
+                } else {
+                    console.warn(`No matching document found for item: ${normalizedItem}`);
+                }
+            });
+        }, '')();
+
+    /**
+     * When in quiz mode, randomly selects a document
+     */
+    randomlyChooseDoc = (doc: Doc, childDocs: Doc[]) => DocumentView.getDocumentView(childDocs[Math.floor(Math.random() * childDocs.length)])?.select(false);
     /**
      * Generates a rubric for evaluating the user's description of the document's text
      * @param doc the doc the user is providing info about
@@ -116,9 +174,11 @@ export class GPTPopup extends ObservableReactComponent<object> {
     generateRubric = (doc: Doc) =>
         StrCast(doc.gptRubric)
             ? Promise.resolve(StrCast(doc.gptRubric))
-            : gptAPICall(StrCast(doc.gptInputText), GPTCallType.RUBRIC)
-                  .then(res => (doc.gptRubric = res))
-                  .catch(err => console.error('GPT call failed', err));
+            : Doc.getDescription(doc).then(desc =>
+                  gptAPICall(desc, GPTCallType.RUBRIC)
+                      .then(res => (doc.gptRubric = res))
+                      .catch(err => console.error('GPT call failed', err))
+              );
 
     /**
      * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
@@ -128,17 +188,18 @@ export class GPTPopup extends ObservableReactComponent<object> {
      */
     generateQuizAnswerAnalysis = (doc: Doc, quizAnswer: string) =>
         this.generateRubric(doc).then(() =>
-            gptAPICall(
-                `Question: ${StrCast(doc.gptInputText)}; 
-                 UserAnswer: ${quizAnswer};
-                 Rubric: ${StrCast(doc.gptRubric)}`,
-                GPTCallType.QUIZ
-            ).then(res => {
-                this._conversationArray.push(res || 'GPT provided no answer');
-                this.onQuizRandom?.();
-            })
-            .catch(err => console.error('GPT call failed', err))
-        ) // prettier-ignore
+            Doc.getDescription(doc).then(desc => 
+                gptAPICall(
+                    `Question: ${desc}; 
+                    UserAnswer: ${quizAnswer};
+                    Rubric: ${StrCast(doc.gptRubric)}`,
+                    GPTCallType.QUIZ
+                ).then(res => {
+                    this._conversationArray.push(res || 'GPT provided no answer');
+                    this.onQuizRandom?.();
+                })
+                .catch(err => console.error('GPT call failed', err))
+        )) // prettier-ignore
 
     /**
      * Generates a response to the user's question about the docs in the collection.
@@ -146,32 +207,33 @@ export class GPTPopup extends ObservableReactComponent<object> {
      * @param userPrompt the user's input that chat will respond to
      */
     generateUserPromptResponse = (userPrompt: string) =>
-        (this._retrieveDocDescriptions ?? Promise.resolve)().then(() =>
-            gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
-                (() => {
-                    switch (this.questionTypeNumberToStyle(questionType)) {
-                        case GPTTypeStyle.AssignTags:
-                        case GPTTypeStyle.Filter:    return gptAPICall(this._documentDescriptions, GPTCallType.SUBSET, userPrompt);
-                        case GPTTypeStyle.SortDocs:  return gptAPICall(this._documentDescriptions, GPTCallType.SORT, userPrompt);
-                        default:                     return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, userPrompt);
-                    } // prettier-ignore
-                })().then(
-                    action(res => {
-                        // Trigger the callback with the result
-                        this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
-                        this._conversationArray.push(
-                            ![GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType))? 
-                            // Extract explanation surrounded by ------ at the top or both at the top and bottom
-                            (res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) ?? [])[1]?.trim() ?? 'No explanation found' : res);
-                    })
-                ).catch(err => console.log(err))
+        gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
+            (async () => {
+                switch (this.questionTypeNumberToStyle(questionType)) {
+                    case GPTTypeStyle.AssignTags:
+                    case GPTTypeStyle.Filter:    return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SUBSET, userPrompt)) ?? "";
+                    case GPTTypeStyle.SortDocs:  return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SORT, userPrompt)) ?? "";
+                    default:                     return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(desc, GPTCallType.INFO, userPrompt));
+                } // prettier-ignore
+            })().then(
+                action(res => {
+                    // Trigger the callback with the result
+                    this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
+                    this._conversationArray.push(
+                        [GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType)) ? res:
+                        // Extract explanation surrounded by the DocSeperator string (defined in GPT.ts) at the top or both at the top and bottom
+                        (res.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`)) ?? [])[1]?.trim() ?? 'No explanation found'
+                    );
+                })
             ).catch(err => console.log(err))
-        ); // prettier-ignore
+        ).catch(err => console.log(err)); // prettier-ignore
 
     /**
      * Generates a Dalle image and uploads it to the server.
      */
-    generateImage = (imgDesc: string) => {
+    generateImage = (imgDesc: string, imgTarget: Doc, addToCollection?: (doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) => {
+        this._imgTargetDoc = imgTarget;
+        this.addDoc = addToCollection;
         this.setImgUrls([]);
         this.setMode(GPTPopupMode.IMAGE);
         this.setVisible(true);
@@ -237,7 +299,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
             _layout_fitWidth: true,
             _layout_autoHeight: true,
         });
-        this.addDoc(newDoc, this._sidebarFieldKey);
+        this.addDoc?.(newDoc, this._sidebarFieldKey);
         const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
         if (anchor) {
             DocUtils.MakeLink(newDoc, anchor, {
@@ -270,7 +332,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
                 Doc.AddToMyOverlay(newDoc);
             } else {
-                this.addToCollection?.(newDoc);
+                this.addDoc?.(newDoc);
             }
             // Create link between prompt and image
             DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
@@ -282,8 +344,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
     gptMenu = () => (
         <div className="btns-wrapper-gpt">
             <Button
-                tooltip="Have ChatGPT sort, tag, define, or filter your documents for you!"
-                text="Modify/Sort Cards!"
+                tooltip="Ask GPT to sort, tag, define, or filter your Docs!"
+                text="Ask GPT"
                 onClick={() => this.setMode(GPTPopupMode.USER_PROMPT)}
                 color={StrCast(Doc.UserDoc().userVariantColor)}
                 type={Type.TERT}
@@ -297,8 +359,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 }}
             />
             <Button
-                tooltip="Test your knowledge with ChatGPT!"
-                text="Quiz Cards!"
+                tooltip="Test your knowledge by verifying answers with ChatGPT"
+                text="Take Quiz"
                 onClick={() => {
                     this._conversationArray = ['Define the selected card!'];
                     this.setMode(GPTPopupMode.QUIZ_RESPONSE);
@@ -386,7 +448,14 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     </div>
                 ))}
             </div>
-            {this._gptProcessing ? null : <IconButton tooltip="Generate Again" onClick={() => this.generateImage(this._imageDescription)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
+            {this._gptProcessing ? null : (
+                <IconButton
+                    tooltip="Generate Again"
+                    onClick={() => this._imgTargetDoc && this.generateImage(this._imageDescription, this._imgTargetDoc, this._addToCollection)}
+                    icon={<FontAwesomeIcon icon="redo-alt" size="lg" />}
+                    color={StrCast(Doc.UserDoc().userVariantColor)}
+                />
+            )}
         </div>
     );
 
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index e62ca4bb8..950d9047c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -7,7 +7,7 @@ import { CollectionViewType, DocumentType } from '../client/documents/DocumentTy
 import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
 import { undoable, UndoManager } from '../client/util/UndoManager';
-import { ClientUtils, incrementTitleCopy } from '../ClientUtils';
+import { ClientUtils, imageUrlToBase64, incrementTitleCopy } from '../ClientUtils';
 import {
     AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks,
     DocAcl, DocCss, DocData, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight,
@@ -22,8 +22,9 @@ import { FieldId, RefField } from './RefField';
 import { RichTextField } from './RichTextField';
 import { listSpec } from './Schema';
 import { ComputedField, ScriptField } from './ScriptField';
-import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types';
+import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types';
 import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
+import { gptImageLabel } from '../client/apis/gpt/GPT';
 
 export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>;
 export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>;
@@ -1467,6 +1468,25 @@ export namespace Doc {
         });
     }
 
+    /**
+     * text description of a Doc. RTF documents will have just their text and pdf documents will have the first 50 words.
+     * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title.
+     * @param doc
+     * @returns
+     */
+    export function getDescription(doc: Doc) {
+        const curDescription = StrCast(doc[DocData][Doc.LayoutFieldKey(doc) + '_description']);
+        const docText = (async (tdoc:Doc) => {
+            switch (tdoc.type) {
+                case DocumentType.PDF: return curDescription || StrCast(tdoc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
+                case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '')
+                                                .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.'));
+                case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text;
+                default:               return StrCast(tdoc.title);
+        }}); // prettier-ignore
+        return docText(doc).then(text => (doc[DocData][Doc.LayoutFieldKey(doc) + '_description'] = text));
+    }
+
     // prettier-ignore
     export function toIcon(doc?: Doc, isOpen?: Opt<boolean>) {
         if (isOpen) return doc?.isFolder ? 'chevron-down' : 'folder-open';
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index e19673665..474882959 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -134,6 +134,10 @@ export function PDFCast(field: FieldResult, defaultVal: PdfField | null = null)
 export function ImageCast(field: FieldResult, defaultVal: ImageField | null = null) {
     return Cast(field, ImageField, defaultVal);
 }
+export function ImageCastWithSuffix(field: FieldResult, suffix: string, defaultVal: ImageField | null = null) {
+    const href = ImageCast(field, defaultVal)?.url.href;
+    return href ? `${href.split('.')[0]}${suffix}.${href.split('.')[1]}` : null;
+}
 
 export function FieldValue<T extends FieldType, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
 // eslint-disable-next-line no-redeclare
-- 
cgit v1.2.3-70-g09d2


From f6581aebd603dcdd84e033acffe46352ce5a4485 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 19 Feb 2025 15:05:47 -0500
Subject: more gptpopup cleanup.

---
 src/client/apis/gpt/GPT.ts                         | 59 +++++++-------
 src/client/util/CurrentUserUtils.ts                |  6 +-
 src/client/util/Import & Export/ImageUtils.ts      | 12 +--
 src/client/views/StyleProviderQuiz.tsx             |  2 +-
 src/client/views/collections/CollectionSubView.tsx | 33 ++++----
 src/client/views/global/globalScripts.ts           | 14 ++--
 src/client/views/nodes/ComparisonBox.tsx           |  6 +-
 .../views/nodes/chatbot/agentsystem/Agent.ts       |  3 +-
 .../nodes/chatbot/chatboxcomponents/ChatBox.tsx    |  3 +-
 .../views/nodes/chatbot/tools/ImageCreationTool.ts | 36 ++++----
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 95 ++++++++++------------
 src/fields/Doc.ts                                  |  2 +-
 12 files changed, 126 insertions(+), 145 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index e1ae99d97..6d9bc1d06 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,12 +1,11 @@
 import { ChatCompletionMessageParam, Image } from 'openai/resources';
 import { openai } from './setup';
 
-export enum GPTTypeStyle {
+export enum GPTDocCommand {
     AssignTags = 1,
     Filter = 2,
-    DocInfo = 3,
-    GeneralInfo = 4,
-    SortDocs = 5,
+    GetInfo = 3,
+    Sort = 4,
 }
 
 export const DescriptionSeperator = '======';
@@ -18,8 +17,6 @@ enum GPTCallType {
     EDIT = 'edit',
     CHATCARD = 'chatcard', // a single flashcard style response to a question
     FLASHCARD = 'flashcard', // a set of flashcard qustion/answer responses to a topic
-    QUIZ = 'quiz',
-    SORT = 'sort',
     DESCRIBE = 'describe',
     MERMAID = 'mermaid',
     DATA = 'data',
@@ -27,15 +24,17 @@ enum GPTCallType {
     PRONUNCIATION = 'pronunciation',
     DRAW = 'draw',
     COLOR = 'color',
-    RUBRIC = 'rubric', // needs to be filled in below
-    TYPE = 'type', // needs to be filled in below
-    SUBSET = 'subset', // needs to be filled in below
-    INFO = 'info', // needs to be filled in below
     TEMPLATE = 'template',
     VIZSUM = 'vizsum',
     VIZSUM2 = 'vizsum2',
     FILL = 'fill',
     COMPLETEPROMPT = 'completeprompt',
+    QUIZDOC = 'quiz_doc',
+    MAKERUBRIC = 'make_rubric', // create a definition rubric for a document to be used when quizzing the user
+    COMMANDTYPE = 'command_type', // Determine the type of command being made (GPTQueryType - eg., AssignTags, Sort, Filter, DocInfo, GenInfo) and possibly some parameters (eg, Tag type for Tags)
+    SUBSETDOCS = 'subset_docs', // select a subset of documents based on their descriptions
+    DOCINFO = 'doc_info', // provide information about a document
+    SORTDOCS = 'sort_docs',
 }
 
 type GPTCallOpts = {
@@ -69,7 +68,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         temp: 0.5,
         prompt: "You are a helpful resarch assistant. Analyze the user's data to find meaningful patterns and/or correlation. Please only return a JSON with a correlation column 1 propert, a correlation column 2 property, and an analysis property. ",
     },
-    sort: {
+    sort_docs: {
         model: 'gpt-4o',
         maxTokens: 2048,
         temp: 0.25,
@@ -89,7 +88,7 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         prompt: 'Make flashcards out of this text with each question and answer labeled as question and answer. Create a title for each question and asnwer that is labeled as "title".  Do not label each flashcard and do not include asterisks: ',
     },
     chatcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Answer the following question as a short flashcard response. Do not include a label.' },
-    quiz: {
+    quiz_doc: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
@@ -138,20 +137,20 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
         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.',
     },
-    type: {
+    command_type: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
         prompt: `I'm going to provide you with a question. 
                  Based on the question, is the user asking you to 
-                     ${GPTTypeStyle.AssignTags}. Assigns docs with tags(like star / heart etc)/labels, 
-                     ${GPTTypeStyle.DocInfo}. Provide information about a specific doc 
-                     ${GPTTypeStyle.Filter}. Filter docs based on a question/information 
-                     ${GPTTypeStyle.GeneralInfo}. Provide general information 
-                     ${GPTTypeStyle.SortDocs}. Put cards in a specific order. 
-                Answer with only the number for 2-5. For number one, provide the number (1) and the appropriate tag`,
+                     ${GPTDocCommand.AssignTags}. Assigns docs with tags(like star / heart etc)/labels.
+                     ${GPTDocCommand.GetInfo}. Provide information about a specific doc.
+                     ${GPTDocCommand.Filter}. Filter docs based on a question/information.
+                     ${GPTDocCommand.Sort}. Put docs in a specific order. 
+                Answer with only the number for ${GPTDocCommand.GetInfo}-${GPTDocCommand.Sort}. 
+                For number one, provide the number (${GPTDocCommand.AssignTags}) and the appropriate tag`,
     },
-    subset: {
+    subset_docs: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
@@ -164,14 +163,14 @@ const callTypeMap: { [type in GPTCallType]: GPTCallOpts } = {
                 It is VERY important that you format it exactly as described, ensuring the proper number of '${DescriptionSeperator[0]}' and '${DocSeperator[0]}' (${DescriptionSeperator.length} of each) and NO commas`,
     },
 
-    info: {
+    doc_info: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
         prompt: `Answer the user's question with a short (<100 word) response. 
                  If a particular document is selected I will provide that information (which may help with your response)`,
     },
-    rubric: {
+    make_rubric: {
         model: 'gpt-4-turbo',
         maxTokens: 1024,
         temp: 0,
@@ -188,17 +187,15 @@ let lastResp = '';
  * @returns AI Output
  */
 const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: string, dontCache?: boolean) => {
-    const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? '.' : '');
+    const inputText = inputTextIn + ([GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZDOC, GPTCallType.STACK].includes(callType) ? '.' : '');
     const opts = callTypeMap[callType];
     if (!opts) {
         console.log('The query type:' + callType + ' requires a configuration.');
         return 'Error connecting with API.';
     }
-    if (lastCall === inputText && dontCache !== true) return lastResp;
+    if (lastCall === inputText && dontCache !== true && lastResp) return lastResp;
     try {
-        lastCall = inputText;
-
-        const usePrompt = prompt ? prompt + opts.prompt : opts.prompt;
+        const usePrompt = prompt ? prompt + '.' + opts.prompt : opts.prompt;
         const messages: ChatCompletionMessageParam[] = [
             { role: 'system', content: usePrompt },
             { role: 'user', content: inputText },
@@ -210,8 +207,12 @@ const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: s
             temperature: opts.temp,
             max_tokens: opts.maxTokens,
         });
-        lastResp = response.choices[0].message.content ?? '';
-        return lastResp;
+        const result = response.choices[0].message.content ?? '';
+        if (!dontCache) {
+            lastResp = result;
+            lastCall = inputText;
+        }
+        return result;
     } catch (err) {
         console.log(err);
         return 'Error connecting with API.';
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 6ca181d92..fb349abd9 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -710,11 +710,7 @@ pie title Minerals in my tap water
             { title: "Type",      icon:"eye",            toolTip:"Sort by document type",                          btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Color",     icon:"palette",        toolTip:"Sort by document color",                         btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color",      funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
             { title: "Tags",      icon:"bolt",           toolTip:"Sort by document's tags",                        btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag",        funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
-            { title: "Sort",      icon: "sort" ,         toolTip: "Manage sort order / lock status",               btnType: ButtonType.MultiToggleButton,               toolType:"alignment", ignoreClick: true,
-              subMenu: [
-                { title: "Ascending", toolTip: "Sort the cards in ascending order", btnType: ButtonType.ToggleButton, icon: "sort-up",  toolType:"up",  ignoreClick: true,  scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
-                { title: "Descending",toolTip: "Sort the cards in descending order",btnType: ButtonType.ToggleButton, icon: "sort-down",toolType:"down",ignoreClick: true,  scripts: {onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
-            ]},
+            { title: "Reverse",   icon: "sort-up",       toolTip: "Sort the cards in reverse order",               btnType: ButtonType.ToggleButton, expertMode: false, toolType:"reverse",    funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
          ]
     }
 
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 8d4eefa7e..f73149fdc 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -4,22 +4,16 @@ import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
+import { Upload } from '../../../server/SharedMediaTypes';
 import { Networking } from '../../Network';
 
 export namespace ImageUtils {
-    export type imgInfo = {
-        contentSize: number;
-        nativeWidth: number;
-        nativeHeight: number;
-        source: string;
-        exifData: { error: string | undefined; data: string };
-    };
-    export const ExtractImgInfo = async (document: Doc): Promise<imgInfo | undefined> => {
+    export const ExtractImgInfo = async (document: Doc): Promise<Upload.InspectionResults | undefined> => {
         const field = Cast(document.data, ImageField);
         return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined;
     };
 
-    export const AssignImgInfo = (document: Doc, data?: imgInfo) => {
+    export const AssignImgInfo = (document: Doc, data?: Upload.InspectionResults) => {
         if (data) {
             data.nativeWidth && (document._height = (NumCast(document._width) * data.nativeHeight) / data.nativeWidth);
             const proto = document[DocData];
diff --git a/src/client/views/StyleProviderQuiz.tsx b/src/client/views/StyleProviderQuiz.tsx
index b3fb8c930..d8eeb3490 100644
--- a/src/client/views/StyleProviderQuiz.tsx
+++ b/src/client/views/StyleProviderQuiz.tsx
@@ -265,7 +265,7 @@ export namespace styleProviderQuiz {
                     '. ' +
                     rubricText +
                     '. One sentence and evaluate based on meaning, not wording. Provide a hex color at the beginning with a period after it on a scale of green (minor details missed) to red (big error) for how correct the answer is. Example: "#FFFFFF. Pasta is delicious."';
-                const response = await gptAPICall(queryText, GPTCallType.QUIZ);
+                const response = await gptAPICall(queryText, GPTCallType.QUIZDOC);
                 const hexSent = extractHexAndSentences(response);
                 doc.quiz = hexSent.sentences?.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
                 doc.backgroundColor = '#' + hexSent.hexNumber;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 954f9aa4c..b40cd2761 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -37,6 +37,9 @@ export enum docSortings {
     Tag = 'tag',
     None = '',
 }
+
+export const ChatSortField = 'chat_sortIndex';
+
 export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
     isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
     isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
@@ -229,23 +232,21 @@ export function CollectionSubView<X>() {
 
         childSortedDocs = (docsIn: Doc[], dragIndex: number) => {
             const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings;
-            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_desc']);
+            const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']);
             const docs = docsIn.slice();
-            if (sortType) {
-                docs.sort((docA, docB) => {
-                    const [typeA, typeB] = (() => {
-                        switch (sortType) {
-                            default:                
-                            case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
-                            case docSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
-                            case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
-                            case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
-                            case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
-                        }
-                    })(); //prettier-ignore
-                    return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
-                });
-            }
+            sortType && docs.sort((docA, docB) => {
+                const [typeA, typeB] = (() => {
+                    switch (sortType) {
+                        default:                
+                        case docSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
+                        case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999),                   NumCast(docB[ChatSortField], 9999)];
+                        case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
+                        case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
+                        case docSortings.Tag:  return [StrListCast(docA.tags).join(""),                      StrListCast(docB.tags).join("")];
+                    }
+                })(); 
+                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1);
+            }); //prettier-ignore
             if (dragIndex !== -1) {
                 const draggedDoc = DragManager.docsBeingDragged[0];
                 const originalIndex = docs.findIndex(doc => doc === draggedDoc);
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 2bc0e3338..79873ed8f 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -152,7 +152,7 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
 
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function showFreeform(
-    attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'toggle-chat' | 'toggle-tags' | 'tag',
+    attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'reverse' | 'toggle-chat' | 'toggle-tags' | 'tag',
     checkResult?: boolean,
     persist?: boolean
 ) {
@@ -163,7 +163,7 @@ ScriptingGlobals.add(function showFreeform(
     }
 
     // prettier-ignore
-    const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down'| 'toggle-chat' | 'toggle-tags' | 'tag', 
+    const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'reverse'| 'toggle-chat' | 'toggle-tags' | 'tag', 
     { 
         waitForRender?: boolean; 
         checkResult: (doc: Doc) => boolean; 
@@ -214,13 +214,9 @@ ScriptingGlobals.add(function showFreeform(
             checkResult: (doc: Doc) => StrCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort"]) === "tag",
             setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort"] === "tag" ? doc[Doc.LayoutFieldKey(doc)+"_sort"] = '' : doc[Doc.LayoutFieldKey(doc)+"_sort"] = docSortings.Tag}, // prettier-ignore
         }],
-        ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
-            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = undefined; },
-        }],
-        ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_desc"]),
-            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_desc"] = true; },
+        ['reverse', {
+            checkResult: (doc: Doc) => BoolCast(doc?.[Doc.LayoutFieldKey(doc)+"_sort_reverse"]),
+            setDoc: (doc: Doc, dv: DocumentView) => { doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"] = !doc[Doc.LayoutFieldKey(doc)+"_sort_reverse"]; },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => SnappingManager.ChatVisible,
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index cb0831d3c..5315612e1 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -291,7 +291,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             this.askGPTPhonemes(this._inputValue);
             this._renderSide = this.backKey;
             this._outputValue = '';
-        } else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
+        } else if (this._inputValue) this.askGPT(GPTCallType.QUIZDOC);
     };
 
     onPointerMove = ({ movementX }: PointerEvent) => {
@@ -511,7 +511,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
      */
     askGPT = async (callType: GPTCallType) => {
         const questionText = this.frontText;
-        const queryText = questionText + (callType == GPTCallType.QUIZ ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
+        const queryText = questionText + (callType == GPTCallType.QUIZDOC ? ' UserAnswer: ' + this._inputValue + '. ' + ' Rubric: ' + this.backText : '');
 
         this.loading = true;
         const res = !this.frontText
@@ -522,7 +522,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                           case GPTCallType.CHATCARD:
                               DocCast(this.dataDoc[this.backKey])[DocData].text = resp;
                               break;
-                          case GPTCallType.QUIZ:
+                          case GPTCallType.QUIZDOC:
                               this._renderSide = this.backKey;
                               this._outputValue = resp.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
                               break;
diff --git a/src/client/views/nodes/chatbot/agentsystem/Agent.ts b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
index b2b0c9aea..2ed808622 100644
--- a/src/client/views/nodes/chatbot/agentsystem/Agent.ts
+++ b/src/client/views/nodes/chatbot/agentsystem/Agent.ts
@@ -22,6 +22,7 @@ import { ChatCompletionMessageParam } from 'openai/resources';
 import { Doc } from '../../../../../fields/Doc';
 import { parsedDoc } from '../chatboxcomponents/ChatBox';
 import { WebsiteInfoScraperTool } from '../tools/WebsiteInfoScraperTool';
+import { Upload } from '../../../../../server/SharedMediaTypes';
 //import { CreateTextDocTool } from '../tools/CreateTextDocumentTool';
 
 dotenv.config();
@@ -61,7 +62,7 @@ export class Agent {
         history: () => string,
         csvData: () => { filename: string; id: string; text: string }[],
         addLinkedUrlDoc: (url: string, id: string) => void,
-        createImage: (result: any, options: DocumentOptions) => void,
+        createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void,
         addLinkedDoc: (doc: parsedDoc) => Doc | undefined,
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         createCSVInDash: (url: string, title: string, id: string, data: string) => void
diff --git a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
index 16da360fc..6e9307d37 100644
--- a/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
+++ b/src/client/views/nodes/chatbot/chatboxcomponents/ChatBox.tsx
@@ -42,6 +42,7 @@ import './ChatBox.scss';
 import MessageComponentBox from './MessageComponent';
 import { ProgressBar } from './ProgressBar';
 import { OpenWhere } from '../../OpenWhere';
+import { Upload } from '../../../../../server/SharedMediaTypes';
 
 dotenv.config();
 
@@ -412,7 +413,7 @@ export class ChatBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         });
 
     @action
-    createImageInDash = async (result: any, options: DocumentOptions) => {
+    createImageInDash = async (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => {
         const newImgSrc =
             result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
                 ? ClientUtils.prepend(result.accessPaths.agnostic.client)
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index 177552c5c..dc6140871 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -1,10 +1,10 @@
-import { v4 as uuidv4 } from 'uuid';
 import { RTFCast } from '../../../../../fields/Types';
 import { DocumentOptions } from '../../../../documents/Documents';
 import { Networking } from '../../../../Network';
 import { ParametersType, ToolInfo } from '../types/tool_types';
 import { Observation } from '../types/types';
 import { BaseTool } from './BaseTool';
+import { Upload } from '../../../../../server/SharedMediaTypes';
 
 const imageCreationToolParams = [
     {
@@ -25,8 +25,8 @@ const imageCreationToolInfo: ToolInfo<ImageCreationToolParamsType> = {
 };
 
 export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
-    private _createImage: (result: any, options: DocumentOptions) => void;
-    constructor(createImage: (result: any, options: DocumentOptions) => void) {
+    private _createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void;
+    constructor(createImage: (result: Upload.FileInformation & Upload.InspectionResults, options: DocumentOptions) => void) {
         super(imageCreationToolInfo);
         this._createImage = createImage;
     }
@@ -42,23 +42,19 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
             });
             console.log('Image generation result:', result);
             this._createImage(result, { text: RTFCast(image_prompt) });
-            if (url) {
-                const id = uuidv4();
-
-                return [
-                    {
-                        type: 'image_url',
-                        image_url: { url },
-                    },
-                ];
-            } else {
-                return [
-                    {
-                        type: 'text',
-                        text: `An error occurred while generating image.`,
-                    },
-                ];
-            }
+            return url
+                ? [
+                      {
+                          type: 'image_url',
+                          image_url: { url },
+                      },
+                  ]
+                : [
+                      {
+                          type: 'text',
+                          text: `An error occurred while generating image.`,
+                      },
+                  ];
         } catch (error) {
             console.log(error);
             return [
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 72381cfad..cb3e9b2d7 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -10,7 +10,7 @@ import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
-import { DescriptionSeperator, DocSeperator, GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { DescriptionSeperator, DocSeperator, GPTCallType, GPTDocCommand, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs } from '../../../documents/Documents';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -18,7 +18,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
 import { undoable } from '../../../util/UndoManager';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { TagItem } from '../../TagsView';
-import { docSortings } from '../../collections/CollectionSubView';
+import { ChatSortField, docSortings } from '../../collections/CollectionSubView';
 import { DocumentView } from '../../nodes/DocumentView';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
@@ -36,6 +36,7 @@ export enum GPTPopupMode {
 export class GPTPopup extends ObservableReactComponent<object> {
     // eslint-disable-next-line no-use-before-define
     static Instance: GPTPopup;
+    static ChatTag = '#chat'; // tag used by GPT popup to filter docs
     private _messagesEndRef: React.RefObject<HTMLDivElement>;
     private _correlatedColumns: string[] = [];
     private _dataChatPrompt: string | undefined = undefined;
@@ -77,10 +78,10 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 if (hasChildDocs) {
                     this._textToDocMap.clear();
                     this.setCollectionContext(selDoc.Document);
-                    this.onGptResponse = (sortResult: string, questionType: GPTTypeStyle, tag?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, tag);
+                    this.onGptResponse = (sortResult: string, questionType: GPTDocCommand, args?: string) => this.processGptResponse(selDoc, this._textToDocMap, sortResult, questionType, args);
                     this.onQuizRandom = () => this.randomlyChooseDoc(selDoc.Document, hasChildDocs());
                     this._documentDescriptions = Promise.all(hasChildDocs().map(doc =>
-                            Doc.getDescription(doc).then(text => this._textToDocMap.set(text, doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
+                            Doc.getDescription(doc).then(text => this._textToDocMap.set(text.trim(), doc) && `${DescriptionSeperator}${text}${DescriptionSeperator}`)
                         )).then(docDescriptions => docDescriptions.join()); // prettier-ignore
                 }
             },
@@ -110,8 +111,8 @@ export class GPTPopup extends ObservableReactComponent<object> {
     @action public setMode = (mode: GPTPopupMode) => (this._mode = mode);
 
     onQuizRandom?: () => void;
-    onGptResponse?: (sortResult: string, questionType: GPTTypeStyle, tag?: string) => void;
-    questionTypeNumberToStyle = (questionType: string) => +questionType.split(' ')[0][0];
+    onGptResponse?: (sortResult: string, questionType: GPTDocCommand, args?: string) => void;
+    NumberToCommandType = (questionType: string) => +questionType.split(' ')[0][0];
 
     /**
      * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
@@ -120,44 +121,38 @@ export class GPTPopup extends ObservableReactComponent<object> {
      * @param questionType
      * @param tag
      */
-    processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTTypeStyle, tag?: string) =>
+    processGptResponse = (docView: DocumentView, textToDocMap: Map<string, Doc>, gptOutput: string, questionType: GPTDocCommand, args?: string) =>
         undoable(() => {
-            // Split the string into individual list items
-            const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
-            if (questionType === GPTTypeStyle.Filter) {
-                docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, '#chat'));
-            }
-
-            if (questionType === GPTTypeStyle.SortDocs) {
-                docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
-            }
-
-            listItems.forEach((item, index) => {
-                const normalizedItem = item.replace(/\n/g, ' ').trim();
-                // find the corresponding Doc in the textToDoc map
-                const doc = textToDocMap.get(normalizedItem);
-                if (doc) {
+            switch (questionType) { // reset collection based on question typefc
+                case GPTDocCommand.Sort:
+                    docView.Document[docView.ComponentView?.fieldKey + '_sort'] = docSortings.Chat;
+                    break;
+                case GPTDocCommand.Filter:
+                    docView.ComponentView?.hasChildDocs?.().forEach(d => TagItem.removeTagFromDoc(d, GPTPopup.ChatTag));
+                    break;
+            } // prettier-ignore
+
+            gptOutput.split('======').filter(item => item.trim() !== '')  // Split output into individual document contents
+                .map(docContentRaw => textToDocMap.get(docContentRaw.replace(/\n/g, ' ').trim())) // the find the corresponding Doc using textToDoc map
+                .filter(doc => doc).map(doc => doc!) // filter out undefined values
+                .forEach((doc, index) => {
                     switch (questionType) {
-                        case GPTTypeStyle.SortDocs:
-                            doc.chatIndex = index;
+                        case GPTDocCommand.Sort:
+                            doc[ChatSortField] = index;
                             break;
-                        case GPTTypeStyle.AssignTags:
-                            if (tag) {
-                                const hashTag = tag.startsWith('#') ? tag : '#' + tag[0].toLowerCase() + tag.slice(1);
-                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(tag)) ?? hashTag;
+                        case GPTDocCommand.AssignTags:
+                            if (args) {
+                                const hashTag = args.startsWith('#') ? args : '#' + args[0].toLowerCase() + args.slice(1);
+                                const filterTag = Doc.MyFilterHotKeys.map(key => StrCast(key.toolType)).find(key => key.includes(args)) ?? hashTag;
                                 TagItem.addTagToDoc(doc, filterTag);
                             }
                             break;
-                        case GPTTypeStyle.Filter:
-                            TagItem.addTagToDoc(doc, '#chat');
-                            Doc.setDocFilter(docView.Document, 'tags', '#chat', 'check');
+                        case GPTDocCommand.Filter:
+                            TagItem.addTagToDoc(doc, GPTPopup.ChatTag);
+                            Doc.setDocFilter(docView.Document, 'tags', GPTPopup.ChatTag, 'check');
                             break;
                     }
-                } else {
-                    console.warn(`No matching document found for item: ${normalizedItem}`);
-                }
-            });
+                }); // prettier-ignore
         }, '')();
 
     /**
@@ -173,7 +168,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         StrCast(doc.gptRubric)
             ? Promise.resolve(StrCast(doc.gptRubric))
             : Doc.getDescription(doc).then(desc =>
-                  gptAPICall(desc, GPTCallType.RUBRIC)
+                  gptAPICall(desc, GPTCallType.MAKERUBRIC)
                       .then(res => (doc.gptRubric = res))
                       .catch(err => console.error('GPT call failed', err))
               );
@@ -191,7 +186,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     `Question: ${desc}; 
                     UserAnswer: ${quizAnswer};
                     Rubric: ${StrCast(doc.gptRubric)}`,
-                    GPTCallType.QUIZ
+                    GPTCallType.QUIZDOC
                 ).then(res => {
                     this._conversationArray.push(res || 'GPT provided no answer');
                     this.onQuizRandom?.();
@@ -205,20 +200,20 @@ export class GPTPopup extends ObservableReactComponent<object> {
      * @param userPrompt the user's input that chat will respond to
      */
     generateUserPromptResponse = (userPrompt: string) =>
-        gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
+        gptAPICall(userPrompt, GPTCallType.COMMANDTYPE, undefined, true).then((commandType, args = commandType.split(' ').slice(1).join(' ')) =>
             (async () => {
-                switch (this.questionTypeNumberToStyle(questionType)) {
-                    case GPTTypeStyle.AssignTags:
-                    case GPTTypeStyle.Filter:    return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SUBSET, userPrompt)) ?? "";
-                    case GPTTypeStyle.SortDocs:  return this._documentDescriptions?.then(descs => gptAPICall(descs, GPTCallType.SORT, userPrompt)) ?? "";
-                    default:                     return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(desc, GPTCallType.INFO, userPrompt));
+                switch (this.NumberToCommandType(commandType)) {
+                    case GPTDocCommand.AssignTags:
+                    case GPTDocCommand.Filter:    return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SUBSETDOCS, descs)) ?? "";
+                    case GPTDocCommand.Sort:      return this._documentDescriptions?.then(descs => gptAPICall(userPrompt, GPTCallType.SORTDOCS, descs)) ?? "";
+                    default:                      return Doc.getDescription(DocumentView.SelectedDocs().lastElement()).then(desc => gptAPICall(userPrompt, GPTCallType.DOCINFO, desc));
                 } // prettier-ignore
             })().then(
                 action(res => {
                     // Trigger the callback with the result
-                    this.onGptResponse?.(res || 'Something went wrong :(', this.questionTypeNumberToStyle(questionType), questionType.split(' ').slice(1).join(' '));
+                    this.onGptResponse?.(res || 'Something went wrong :(', this.NumberToCommandType(commandType), args);
                     this._conversationArray.push(
-                        [GPTTypeStyle.GeneralInfo, GPTTypeStyle.DocInfo].includes(this.questionTypeNumberToStyle(questionType)) ? res:
+                        this.NumberToCommandType(commandType) === GPTDocCommand.GetInfo ? res:
                         // Extract explanation surrounded by the DocSeperator string (defined in GPT.ts) at the top or both at the top and bottom
                         (res.match(new RegExp(`${DocSeperator}\\s*([\\s\\S]*?)\\s*(?:${DocSeperator}|$)`)) ?? [])[1]?.trim() ?? 'No explanation found'
                     );
@@ -583,10 +578,10 @@ export class GPTPopup extends ObservableReactComponent<object> {
                         tooltip="Clear Chat filter"
                         toggleType={ToggleType.BUTTON}
                         type={Type.PRIM}
-                        toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat')}
-                        text={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat') ? 'filtered' : ''}
-                        color={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat') ? 'red' : 'transparent'}
-                        onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', '#chat', 'remove')}
+                        toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag)}
+                        text={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'filtered' : ''}
+                        color={Doc.hasDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag) ? 'red' : 'transparent'}
+                        onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', GPTPopup.ChatTag, 'remove')}
                     />
                     {(this._mode === GPTPopupMode.USER_PROMPT || this._mode === GPTPopupMode.QUIZ_RESPONSE) && (
                         <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.GPT_MENU)} />
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 950d9047c..bdd41b0bb 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1482,7 +1482,7 @@ export namespace Doc {
                 case DocumentType.IMG: return curDescription || imageUrlToBase64(ImageCastWithSuffix(Doc.LayoutField(tdoc), '_o') ?? '')
                                                 .then(hrefBase64 => gptImageLabel(hrefBase64, 'Give three to five labels to describe this image.'));
                 case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutFieldKey(tdoc)]).Text;
-                default:               return StrCast(tdoc.title);
+                default:               return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title);
         }}); // prettier-ignore
         return docText(doc).then(text => (doc[DocData][Doc.LayoutFieldKey(doc) + '_description'] = text));
     }
-- 
cgit v1.2.3-70-g09d2


From a9a1a6a507616a77f70d6525dab5027f5b7a60e6 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 26 Feb 2025 20:48:51 -0500
Subject: added typing to PostToServer calls.  made smartDraw popup create
 images locally.

---
 src/client/Network.ts                              |  2 +-
 src/client/apis/GoogleAuthenticationManager.tsx    | 27 ++++---
 .../apis/google_docs/GoogleApiClientUtils.ts       | 19 ++---
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 18 ++---
 src/client/util/Import & Export/ImageUtils.ts      |  4 +-
 src/client/util/PingManager.ts                     |  2 +-
 src/client/util/SettingsManager.tsx                |  3 +-
 src/client/views/collections/CollectionSubView.tsx |  3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 +-
 .../views/nodes/DataVizBox/DocCreatorMenu.tsx      |  3 +-
 src/client/views/nodes/ImageBox.tsx                | 20 +++--
 .../views/nodes/RecordingBox/RecordingView.tsx     |  3 +-
 .../views/nodes/chatbot/tools/CreateCSVTool.ts     |  4 +-
 .../views/nodes/chatbot/tools/ImageCreationTool.ts |  4 +-
 src/client/views/nodes/chatbot/tools/RAGTool.ts    |  2 +-
 src/client/views/nodes/chatbot/tools/SearchTool.ts |  8 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 13 ++-
 src/client/views/smartdraw/FireflyConstants.ts     |  5 ++
 src/client/views/smartdraw/SmartDrawHandler.tsx    | 94 ++++++++++------------
 19 files changed, 123 insertions(+), 116 deletions(-)

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

diff --git a/src/client/Network.ts b/src/client/Network.ts
index 323a2648c..a2ecf1bea 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -22,7 +22,7 @@ export namespace Networking {
             },
             body: body ? JSON.stringify(body) : undefined,
         }).then(async response => {
-            if (response.ok) return response.json();
+            if (response.ok) return response.json() as object;
 
             return await response.text().then(text => ({ error: '' + response.status + ':' + response.statusText + '-' + text }));
         });
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 5269f763b..94ce42d8d 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -11,7 +11,8 @@ const AuthenticationUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
 const prompt = 'Paste authorization code here...';
 
 @observer
-export class GoogleAuthenticationManager extends React.Component<{}> {
+export class GoogleAuthenticationManager extends React.Component<object> {
+    // eslint-disable-next-line no-use-before-define
     public static Instance: GoogleAuthenticationManager;
     private authenticationLink: Opt<string> = undefined;
     @observable private openState = false;
@@ -19,7 +20,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
     @observable private showPasteTargetState = false;
     @observable private success: Opt<boolean> = undefined;
     @observable private displayLauncher = true;
-    @observable private credentials: any;
+    @observable private credentials: { user_info: { name: string; picture: string }; access_token: string } | undefined = undefined;
     private disposer: Opt<IReactionDisposer>;
 
     private set isOpen(value: boolean) {
@@ -35,25 +36,25 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
     }
 
     public fetchOrGenerateAccessToken = async (displayIfFound = false) => {
-        let response: any = await Networking.FetchFromServer('/readGoogleAccessToken');
+        const response = await Networking.FetchFromServer('/readGoogleAccessToken');
         // if this is an authentication url, activate the UI to register the new access token
         if (new RegExp(AuthenticationUrl).test(response)) {
             this.isOpen = true;
             this.authenticationLink = response;
-            return new Promise<string>(async resolve => {
+            return new Promise<string>(resolve => {
                 this.disposer?.();
                 this.disposer = reaction(
                     () => this.authenticationCode,
                     async authenticationCode => {
                         if (authenticationCode && /\d{1}\/[\w-]{55}/.test(authenticationCode)) {
                             this.disposer?.();
-                            const response = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode });
+                            const response2 = await Networking.PostToServer('/writeGoogleAccessToken', { authenticationCode });
                             runInAction(() => {
                                 this.success = true;
-                                this.credentials = response;
+                                this.credentials = response2 as { user_info: { name: string; picture: string }; access_token: string };
                             });
                             this.resetState();
-                            resolve(response.access_token);
+                            resolve((response2 as { access_token: string }).access_token);
                         }
                     }
                 );
@@ -61,16 +62,16 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
         }
 
         // otherwise, we already have a valid, stored access token and user info
-        response = JSON.parse(response);
+        const response2 = JSON.parse(response) as { user_info: { name: string; picture: string }; access_token: string };
         if (displayIfFound) {
             runInAction(() => {
                 this.success = true;
-                this.credentials = response;
+                this.credentials = response2;
             });
             this.resetState(-1, -1);
             this.isOpen = true;
         }
-        return response.access_token;
+        return (response2 as { access_token: string }).access_token;
     };
 
     resetState = action((visibleForMS: number = 3000, fadesOutInMS: number = 500) => {
@@ -106,7 +107,7 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
         }
     });
 
-    constructor(props: {}) {
+    constructor(props: object) {
         super(props);
         GoogleAuthenticationManager.Instance = this;
     }
@@ -128,8 +129,8 @@ export class GoogleAuthenticationManager extends React.Component<{}> {
                 {this.showPasteTargetState ? <input className={'paste-target'} onChange={action(e => (this.authenticationCode = e.currentTarget.value))} placeholder={prompt} /> : null}
                 {this.credentials ? (
                     <>
-                        <img className={'avatar'} src={this.credentials.userInfo.picture} />
-                        <span className={'welcome'}>Welcome to Dash, {this.credentials.userInfo.name}</span>
+                        <img className={'avatar'} src={this.credentials.user_info.picture} />
+                        <span className={'welcome'}>Welcome to Dash, {this.credentials.user_info.name}</span>
                         <div
                             className={'disconnect'}
                             onClick={async () => {
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 0b303eacf..c1ac352b1 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,7 +1,5 @@
-/* eslint-disable no-restricted-syntax */
 /* eslint-disable no-use-before-define */
 import { docs_v1 as docsV1 } from 'googleapis';
-// eslint-disable-next-line node/no-deprecated-api
 import { isArray } from 'util';
 import { EditorState } from 'prosemirror-state';
 import { Opt } from '../../../fields/Doc';
@@ -37,7 +35,7 @@ export namespace GoogleApiClientUtils {
             text: string | string[];
             requests: docsV1.Schema$Request[];
         }
-        export type IdHandler = (id: DocumentId) => any;
+        export type IdHandler = (id: DocumentId) => unknown;
         export type CreationResult = Opt<DocumentId>;
         export type ReadLinesResult = Opt<{ title?: string; bodyLines?: string[] }>;
         export type ReadResult = { title: string; body: string };
@@ -145,7 +143,7 @@ export namespace GoogleApiClientUtils {
                     if (paragraphs.length) {
                         const target = paragraphs[paragraphs.length - 1];
                         if (target.paragraph && target.paragraph.elements) {
-                            length = target.paragraph.elements.length;
+                            const length = target.paragraph.elements.length;
                             if (length) {
                                 const final = target.paragraph.elements[length - 1];
                                 return final.endIndex ? final.endIndex - 1 : undefined;
@@ -208,13 +206,13 @@ export namespace GoogleApiClientUtils {
             });
 
         export const setStyle = async (options: UpdateOptions) => {
-            const replies: any = await update({
+            const replies = await update({
                 documentId: options.documentId,
                 requests: options.requests,
             });
-            if ('errors' in replies) {
+            if (replies && 'errors' in replies) {
                 console.log('Write operation failed:');
-                console.log(replies.errors.map((error: any) => error.message));
+                console.log(replies); //.errors.map((error: any) => error.message));
             }
             return replies;
         };
@@ -229,7 +227,6 @@ export namespace GoogleApiClientUtils {
             const { mode } = options;
             if (!(index && mode === WriteMode.Insert)) {
                 const schema = await retrieve({ documentId });
-                // eslint-disable-next-line no-cond-assign
                 if (!schema || !(index = Utils.endOf(schema))) {
                     return undefined;
                 }
@@ -258,10 +255,10 @@ export namespace GoogleApiClientUtils {
                 return undefined;
             }
             requests.push(...options.content.requests);
-            const replies: any = await update({ documentId, requests });
-            if ('errors' in replies) {
+            const replies = await update({ documentId, requests });
+            if (replies && 'errors' in replies) {
                 console.log('Write operation failed:');
-                console.log(replies.errors.map((error: any) => error.message));
+                console.log(replies); // .errors.map((error: any) => error.message));
             }
             return replies;
         };
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index b238f07e9..4b86a8341 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,5 +1,5 @@
 /* eslint-disable no-use-before-define */
-import Photos = require('googlephotos');
+import Photos from 'googlephotos';
 import { AssertionError } from 'assert';
 import { EditorState } from 'prosemirror-state';
 import { ClientUtils } from '../../../ClientUtils';
@@ -118,7 +118,7 @@ export namespace GooglePhotos {
     }
 
     export namespace Import {
-        export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc;
+        export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: unknown[]) => Doc;
 
         export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => {
             await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
@@ -147,7 +147,7 @@ export namespace GooglePhotos {
             values.forEach(async value => {
                 const searched = (await ContentSearch({ included: [value] }))?.mediaItems?.map(({ id }) => id);
                 searched?.forEach(async id => {
-                    const image = await Cast(idMapping[id], Doc);
+                    const image = await Cast(idMapping[id as string], Doc);
                     if (image) {
                         const key = image[Id];
                         const tags = tagMapping.get(key);
@@ -193,7 +193,7 @@ export namespace GooglePhotos {
         }
 
         export interface SearchResponse {
-            mediaItems: any[];
+            mediaItems: MediaItem[];
             nextPageToken: string;
         }
 
@@ -204,7 +204,7 @@ export namespace GooglePhotos {
             const found = 0;
             do {
                 // eslint-disable-next-line no-await-in-loop
-                const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored);
+                const response = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored);
                 mediaItems.push(...response.mediaItems);
                 nextPageTokenStored = response.nextPageToken;
             } while (found);
@@ -278,9 +278,9 @@ export namespace GooglePhotos {
             return undefined;
         };
 
-        export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
+        export const WriteMediaItemsToServer = async (body: { mediaItems: MediaItem[] }): Promise<UploadInformation[]> => {
             const uploads = await Networking.PostToServer('/googlePhotosMediaGet', body);
-            return uploads;
+            return uploads as UploadInformation[];
         };
 
         export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption') => {
@@ -320,7 +320,7 @@ export namespace GooglePhotos {
                 });
             if (media.length) {
                 const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album });
-                return results;
+                return results as Opt<ImageUploadResults>;
             }
             return undefined;
         };
@@ -331,7 +331,7 @@ export namespace GooglePhotos {
             if (typeof target === 'string') {
                 description = target;
             } else if (target instanceof RichTextField) {
-                description = RichTextUtils.ToPlainText(EditorState.fromJSON(new FormattedTextBox({} as any).config, JSON.parse(target.Data)));
+                description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.MakeConfig(undefined, undefined), JSON.parse(target.Data)));
             }
             return description;
         };
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index f73149fdc..43807397f 100644
--- a/src/client/util/Import & Export/ImageUtils.ts	
+++ b/src/client/util/Import & Export/ImageUtils.ts	
@@ -8,9 +8,9 @@ import { Upload } from '../../../server/SharedMediaTypes';
 import { Networking } from '../../Network';
 
 export namespace ImageUtils {
-    export const ExtractImgInfo = async (document: Doc): Promise<Upload.InspectionResults | undefined> => {
+    export const ExtractImgInfo = async (document: Doc) => {
         const field = Cast(document.data, ImageField);
-        return field ? Networking.PostToServer('/inspectImage', { source: field.url.href }) : undefined;
+        return field ? (Networking.PostToServer('/inspectImage', { source: field.url.href }) as Promise<Upload.InspectionResults>) : undefined;
     };
 
     export const AssignImgInfo = (document: Doc, data?: Upload.InspectionResults) => {
diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts
index 170632836..0e4f8cab0 100644
--- a/src/client/util/PingManager.ts
+++ b/src/client/util/PingManager.ts
@@ -29,7 +29,7 @@ export class PingManager {
         });
         Networking.PostToServer('/ping', { date: new Date() })
             .then(res => {
-                SnappingManager.SetServerVersion(res.message);
+                SnappingManager.SetServerVersion((res as { message: string }).message);
                 !this.IsBeating && setIsBeating(true);
             })
             .catch(() => this.IsBeating && setIsBeating(false));
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 5d041f7b4..6ea242fc3 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -598,7 +598,8 @@ export class SettingsManager extends React.Component<object> {
             });
         } else {
             const passwordBundle = { curr_pass: this._curr_password, new_pass: this._new_password, new_confirm: this._new_confirm };
-            const { error } = await Networking.PostToServer('/internalResetPassword', passwordBundle);
+            const reset = await Networking.PostToServer('/internalResetPassword', passwordBundle);
+            const { error } = reset as { error: { msg: string }[] };
             runInAction(() => {
                 this._passwordResultText = error ? 'Error: ' + error[0].msg + '...' : 'Password successfully updated!';
             });
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b40cd2761..ca830aa6f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -28,6 +28,7 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FlashcardPracticeUI } from './FlashcardPracticeUI';
 import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
+import { Upload } from '../../../server/SharedMediaTypes';
 
 export enum docSortings {
     Time = 'time',
@@ -407,7 +408,7 @@ export function CollectionSubView<X>() {
                         const imgSrc = img.split('src="')[1].split('"')[0];
                         const imgOpts = { ...options, _width: 300 };
                         if (imgSrc.startsWith('data:image') && imgSrc.includes('base64')) {
-                            const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })).lastElement();
+                            const result = ((await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })) as Upload.ImageInformation[]).lastElement();
                             const newImgSrc =
                                 result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 //
                                     ? ClientUtils.prepend(result.accessPaths.agnostic.client)
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3c31b584e..b33c267ee 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1291,7 +1291,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     /**
      * Adds the created drawing to the freeform canvas and sets the metadata.
      */
-    addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
+    addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => {
         const docData = doc[DocData];
         docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
         docData._width = opts.size;
@@ -1302,6 +1302,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         docData.ai_drawing_data = gptRes;
         docData.ai = 'gpt';
         this._drawingContainer = doc;
+        if (x !== undefined && y !== undefined) {
+            [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(x, y);
+        }
         this.addDocument(doc);
         this._batch?.end();
     };
diff --git a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
index 7fc906e59..4d9173211 100644
--- a/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
+++ b/src/client/views/nodes/DataVizBox/DocCreatorMenu.tsx
@@ -29,6 +29,7 @@ import './DocCreatorMenu.scss';
 import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider';
 import { Transform } from '../../../util/Transform';
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { Upload } from '../../../../server/SharedMediaTypes';
 
 export enum LayoutType {
     Stacked = 'stacked',
@@ -536,7 +537,7 @@ export class DocCreatorMenu extends ObservableReactComponent<FieldViewProps> {
             const res = await gptImageCall(prompt);
 
             if (res) {
-                const result = await Networking.PostToServer('/uploadRemoteImage', { sources: res });
+                const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: res })) as Upload.FileInformation[];
                 const source = ClientUtils.prepend(result[0].accessPaths.agnostic.client);
                 return source;
             }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b6b0e4a8a..9395863d8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -36,7 +36,7 @@ import { OverlayView } from '../OverlayView';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { PinDocView, PinProps } from '../PinFuncs';
 import { DrawingFillHandler } from '../smartdraw/DrawingFillHandler';
-import { FireflyImageData } from '../smartdraw/FireflyConstants';
+import { FireflyImageData, isFireflyImageData } from '../smartdraw/FireflyConstants';
 import { SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import { StickerPalette } from '../smartdraw/StickerPalette';
 import { StyleProp } from '../StyleProp';
@@ -354,7 +354,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             const ext = extname(file);
                             return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
                         })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
-                    }).then((info: Upload.ImageInformation) => {
+                    }).then(res => {
+                        const info = res as Upload.ImageInformation;
                         const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
                         DocUtils.assignImageInfo(info, img);
                         this._props.addDocTab(img, OpenWhere.addRight);
@@ -627,14 +628,15 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                 } else
                                     SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput || StrCast(this.Document.title), true).then(
                                         action(newImgs => {
-                                            if (newImgs[0] && !(newImgs[0] instanceof Doc)) {
-                                                const url = newImgs[0].pathname;
+                                            const firstImg = newImgs[0];
+                                            if (isFireflyImageData(firstImg)) {
+                                                const url = firstImg.pathname;
                                                 const imgField = new ImageField(url);
                                                 this._prevImgs.length === 0 &&
                                                     this._prevImgs.push({ prompt: StrCast(this.dataDoc.ai_firefly_prompt), seed: this.dataDoc.ai_firefly_seed as number, href: this.paths.lastElement(), pathname: field.url.pathname });
-                                                this._prevImgs.unshift({ prompt: newImgs[0].prompt, seed: newImgs[0].seed, pathname: url });
+                                                this._prevImgs.unshift({ prompt: firstImg.prompt, seed: firstImg.seed, pathname: url });
                                                 this.dataDoc.ai_firefly_history = JSON.stringify(this._prevImgs);
-                                                this.dataDoc.ai_firefly_prompt = newImgs[0].prompt;
+                                                this.dataDoc.ai_firefly_prompt = firstImg.prompt;
                                                 this.dataDoc[this.fieldKey] = imgField;
                                                 this._regenerateLoading = false;
                                                 this._regenInput = '';
@@ -688,7 +690,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                                     const ext = extname(file);
                                     return file.replace(ext, (this._error ? '_o' : this._curSuffix) + ext);
                                 })(ImageCast(this.Document[Doc.LayoutFieldKey(this.Document)])?.url.href),
-                            }).then((info: Upload.ImageInformation) => {
+                            }).then(res => {
+                                const info = res as Upload.ImageInformation;
                                 const img = Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { title: 'expand:' + this.Document.title });
                                 DocUtils.assignImageInfo(info, img);
                                 const genratedDocs = this.Document.generatedDocs
@@ -741,7 +744,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     focus = (anchor: Doc, options: FocusViewOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
 
     renderedPixelDimensions = async () => {
-        const { nativeWidth: width, nativeHeight: height } = await Networking.PostToServer('/inspectImage', { source: this.paths[0] });
+        const res = await Networking.PostToServer('/inspectImage', { source: this.paths[0] });
+        const { nativeWidth: width, nativeHeight: height } = res as { nativeWidth: number; nativeHeight: number };
         return { width, height };
     };
     savedAnnotations = () => this._savedAnnotations;
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 37ffca2d6..e7a6193d4 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/button-has-type */
 import * as React from 'react';
 import { useEffect, useRef, useState } from 'react';
 import { IconContext } from 'react-icons';
@@ -72,7 +71,7 @@ export function RecordingView(props: IRecordingViewProps) {
                 const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({ file })))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server));
 
                 // concat the segments together using post call
-                const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
+                const result = (await Networking.PostToServer('/concatVideos', serverPaths)) as Upload.AccessPathInfo | Error;
                 !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error('video conversion failed');
             })();
         }
diff --git a/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts b/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts
index e8ef3fbfe..290c48d6c 100644
--- a/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts
+++ b/src/client/views/nodes/chatbot/tools/CreateCSVTool.ts
@@ -38,10 +38,10 @@ export class CreateCSVTool extends BaseTool<CreateCSVToolParamsType> {
     async execute(args: ParametersType<CreateCSVToolParamsType>): Promise<Observation[]> {
         try {
             console.log('Creating CSV file:', args.filename, ' with data:', args.csvData);
-            const { fileUrl, id } = await Networking.PostToServer('/createCSV', {
+            const { fileUrl, id } = (await Networking.PostToServer('/createCSV', {
                 filename: args.filename,
                 data: args.csvData,
-            });
+            })) as { fileUrl: string; id: string };
 
             this._handleCSVResult(fileUrl, args.filename, id, args.csvData);
 
diff --git a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
index dc6140871..e92d87dfd 100644
--- a/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
+++ b/src/client/views/nodes/chatbot/tools/ImageCreationTool.ts
@@ -37,9 +37,9 @@ export class ImageCreationTool extends BaseTool<ImageCreationToolParamsType> {
         console.log(`Generating image for prompt: ${image_prompt}`);
         // Create an array of promises, each one handling a search for a query
         try {
-            const { result, url } = await Networking.PostToServer('/generateImage', {
+            const { result, url } = (await Networking.PostToServer('/generateImage', {
                 image_prompt,
-            });
+            })) as { result: Upload.FileInformation & Upload.InspectionResults; url: string };
             console.log('Image generation result:', result);
             this._createImage(result, { text: RTFCast(image_prompt) });
             return url
diff --git a/src/client/views/nodes/chatbot/tools/RAGTool.ts b/src/client/views/nodes/chatbot/tools/RAGTool.ts
index 2db61c768..ef374ed22 100644
--- a/src/client/views/nodes/chatbot/tools/RAGTool.ts
+++ b/src/client/views/nodes/chatbot/tools/RAGTool.ts
@@ -75,7 +75,7 @@ export class RAGTool extends BaseTool<RAGToolParamsType> {
 
     async getFormattedChunks(relevantChunks: RAGChunk[]): Promise<Observation[]> {
         try {
-            const { formattedChunks } = await Networking.PostToServer('/formatChunks', { relevantChunks });
+            const { formattedChunks } = await Networking.PostToServer('/formatChunks', { relevantChunks }) as { formattedChunks: Observation[]}
 
             if (!formattedChunks) {
                 throw new Error('Failed to format chunks');
diff --git a/src/client/views/nodes/chatbot/tools/SearchTool.ts b/src/client/views/nodes/chatbot/tools/SearchTool.ts
index 5fc6ab768..6a11407a5 100644
--- a/src/client/views/nodes/chatbot/tools/SearchTool.ts
+++ b/src/client/views/nodes/chatbot/tools/SearchTool.ts
@@ -41,15 +41,15 @@ export class SearchTool extends BaseTool<SearchToolParamsType> {
         // Create an array of promises, each one handling a search for a query
         const searchPromises = queries.map(async query => {
             try {
-                const { results } = await Networking.PostToServer('/getWebSearchResults', {
+                const { results } = (await Networking.PostToServer('/getWebSearchResults', {
                     query,
                     max_results: this._max_results,
-                });
+                })) as { results: { url: string; snippet: string }[] };
                 const data = results.map((result: { url: string; snippet: string }) => {
                     const id = uuidv4();
                     this._addLinkedUrlDoc(result.url, id);
                     return {
-                        type: 'text',
+                        type: 'text' as const,
                         text: `<chunk chunk_id="${id}" chunk_type="url"><url>${result.url}</url><overview>${result.snippet}</overview></chunk>`,
                     };
                 });
@@ -58,7 +58,7 @@ export class SearchTool extends BaseTool<SearchToolParamsType> {
                 console.log(error);
                 return [
                     {
-                        type: 'text',
+                        type: 'text' as const,
                         text: `An error occurred while performing the web search for query: ${query}`,
                     },
                 ];
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index efddfb841..f4ab2f41c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -21,11 +21,13 @@ import { DictationButton } from '../../DictationButton';
 import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { TagItem } from '../../TagsView';
 import { ChatSortField, docSortings } from '../../collections/CollectionSubView';
-import { DocumentView } from '../../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal } from '../../nodes/DocumentView';
 import { SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
 import { FireflyImageDimensions } from '../../smartdraw/FireflyConstants';
+import { Upload } from '../../../../server/SharedMediaTypes';
+import { OpenWhere } from '../../nodes/OpenWhere';
 
 export enum GPTPopupMode {
     SUMMARY, // summary of seleted document text
@@ -210,7 +212,12 @@ export class GPTPopup extends ObservableReactComponent<object> {
         //     );
         // }  else∂
         return SmartDrawHandler.CreateWithFirefly(imgDesc, FireflyImageDimensions.Square, 0)
-            .then(action(() => (this._userPrompt = '')))
+            .then(
+                action(doc => {
+                    doc instanceof Doc && DocumentViewInternal.addDocTabFunc(doc, OpenWhere.addRight);
+                    this._userPrompt = '';
+                })
+            )
             .catch(e => {
                 alert(e);
                 return undefined;
@@ -259,7 +266,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
             .then(imageUrls =>
                 imageUrls?.[0]
                     ? Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }).then(res => {
-                          const source = ClientUtils.prepend(res[0].accessPaths.agnostic.client);
+                          const source = ClientUtils.prepend((res as Upload.FileInformation[])[0].accessPaths.agnostic.client);
                           return this.setImgUrls([[imageUrls[0]!, source]]);
                       })
                     : undefined
diff --git a/src/client/views/smartdraw/FireflyConstants.ts b/src/client/views/smartdraw/FireflyConstants.ts
index 1f1781617..8cc9e36a5 100644
--- a/src/client/views/smartdraw/FireflyConstants.ts
+++ b/src/client/views/smartdraw/FireflyConstants.ts
@@ -5,6 +5,11 @@ export interface FireflyImageData {
     href?: string;
 }
 
+export function isFireflyImageData(obj: unknown): obj is FireflyImageData {
+    const tobj = obj as FireflyImageData;
+    return typeof obj === 'object' && obj !== null && typeof tobj.pathname === 'string' && typeof tobj.prompt === 'string' && typeof tobj.seed === 'number';
+}
+
 export enum FireflyImageDimensions {
     Square = 'square',
     Landscape = 'landscape',
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index 9d67d111b..532391ac6 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -23,10 +23,10 @@ import { SVGToBezier, SVGType } from '../../util/bezierFit';
 import { InkingStroke } from '../InkingStroke';
 import { ObservableReactComponent } from '../ObservableReactComponent';
 import { MarqueeView } from '../collections/collectionFreeForm';
-import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveInkColor, ActiveInkDash, ActiveInkFillColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
-import { OpenWhere } from '../nodes/OpenWhere';
+import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveInkColor, ActiveInkDash, ActiveInkFillColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
 import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from './FireflyConstants';
 import './SmartDrawHandler.scss';
+import { Upload } from '../../../server/SharedMediaTypes';
 
 export interface DrawingOptions {
     text: string;
@@ -60,7 +60,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
     private _lastResponse: string = '';
     private _selectedDocs: Doc[] = [];
-    private _errorOccurredOnce = false;
 
     @observable private _display: boolean = false;
     @observable private _pageX: number = 0;
@@ -95,7 +94,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added
         or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.)
     */
-    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
+    public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => void = unimplementedFunction;
     public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction;
     /**
      * This creates the ink document that represents a drawing, so it goes through the strokes that make up the drawing,
@@ -206,16 +205,15 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         this._isLoading = true;
         this._canInteract = false;
         if (this.ShowRegenerate) {
-            await this.regenerate(this._selectedDocs);
-            runInAction(() => {
-                this._selectedDocs = [];
-                this._regenInput = '';
-                this._showEditBox = false;
-            });
+            await this.regenerate(this._selectedDocs).then(
+                action(() => {
+                    this._selectedDocs = [];
+                    this._regenInput = '';
+                    this._showEditBox = false;
+                })
+            );
         } else {
-            runInAction(() => {
-                this._showOptions = false;
-            });
+            this._showOptions = false;
             try {
                 if (this._generateImage) {
                     await this.createImageWithFirefly(this._userInput);
@@ -224,18 +222,8 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
                 }
                 this.hideSmartDrawHandler();
-
-                runInAction(() => {
-                    this.ShowRegenerate = true;
-                });
             } catch (err) {
-                if (this._errorOccurredOnce) {
-                    console.error('GPT call failed', err);
-                    this._errorOccurredOnce = false;
-                } else {
-                    this._errorOccurredOnce = true;
-                    await this.handleSendClick();
-                }
+                console.error('GPT call failed', err);
             }
         }
         runInAction(() => {
@@ -256,7 +244,6 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
                 drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
                 drawingDoc && this._selectedDocs.push(drawingDoc);
-                this._errorOccurredOnce = false;
                 return strokeData;
             } else {
                 console.error('GPT call failed');
@@ -270,7 +257,10 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     createImageWithFirefly = (input: string, seed?: number): Promise<FireflyImageData | Doc | undefined> => {
         this._lastInput.text = input;
-        return SmartDrawHandler.CreateWithFirefly(input, this._imgDims, seed);
+        return SmartDrawHandler.CreateWithFirefly(input, this._imgDims, seed).then(doc => {
+            doc instanceof Doc && this.AddDrawing(doc, this._lastInput, input, this._pageX, this._pageY);
+            return doc;
+        });
     }; /**
      * Calls Firefly API to create an image based on user input
      */
@@ -281,9 +271,11 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     public static ReCreateWithFirefly(input: string, imgDims: FireflyImageDimensions, seed?: number): Promise<FireflyImageData | Doc | undefined> {
         const dims = FireflyDimensionsMap[imgDims];
         return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed })
-            .then(img => {
-                if (img.error) {
-                    alert('recreate image failed: ' + img.error);
+            .then(res => {
+                const img = res as Upload.FileInformation;
+                const error = res as { error: string };
+                if ('error' in error) {
+                    alert('recreate image failed: ' + error.error);
                     return undefined;
                 }
                 return { prompt: input, seed, pathname: img.accessPaths.agnostic.client };
@@ -296,21 +288,22 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     public static CreateWithFirefly(input: string, imgDims: FireflyImageDimensions, seed?: number): Promise<FireflyImageData | Doc | undefined> {
         const dims = FireflyDimensionsMap[imgDims];
         return Networking.PostToServer('/queryFireflyImage', { prompt: input, width: dims.width, height: dims.height, seed })
-            .then(img => {
-                if (img.error) {
-                    alert('create image failed: ' + img.error);
+            .then(res => {
+                const img = res as Upload.FileInformation;
+                const error = res as { error: string };
+                if ('error' in error) {
+                    alert('create image failed: ' + error.error);
                     return undefined;
                 }
-                const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)[1];
+                const newseed = img.accessPaths.agnostic.client.match(/\/(\d+)upload/)?.[1];
                 const imgDoc: Doc = Docs.Create.ImageDocument(img.accessPaths.agnostic.client, {
                     title: input.match(/^(.*?)~~~.*$/)?.[1] || input,
                     nativeWidth: dims.width,
                     nativeHeight: dims.height,
                     ai: 'firefly',
-                    ai_firefly_seed: newseed,
+                    ai_firefly_seed: +(newseed ?? 0),
                     ai_firefly_prompt: input,
                 });
-                DocumentViewInternal.addDocTabFunc(imgDoc, OpenWhere.addRight);
                 return imgDoc;
             })
             .catch(e => {
@@ -331,26 +324,21 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
         return Promise.all(
             drawingDocs.map(async doc => {
                 switch (doc.type) {
-                    case DocumentType.IMG:
-                        if (this._regenInput) {
-                            // if (this._selectedDoc) {
-                            const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
-                            return changeInPlace ? this.recreateImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed)) : this.createImageWithFirefly(newPrompt, NumCast(doc?.ai_firefly_seed));
-                            // }
-                        }
-                        return changeInPlace
-                            ? this.recreateImageWithFirefly(this._lastInput.text || StrCast(doc.ai_firefly_prompt), NumCast(doc?.ai_firefly_seed))
-                            : this.createImageWithFirefly(this._lastInput.text || StrCast(doc.ai_firefly_prompt), NumCast(doc?.ai_firefly_seed));
+                    case DocumentType.IMG: {
+                        const func = changeInPlace ? this.recreateImageWithFirefly : this.createImageWithFirefly;
+                        const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput;
+                        return this._regenInput ? func(newPrompt, NumCast(doc?.ai_firefly_seed)) : func(this._lastInput.text || StrCast(doc.ai_firefly_prompt));
+                    }
                     case DocumentType.COL: {
                         try {
-                            let res;
-                            if (this._regenInput) {
-                                const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
-                                res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
-                                this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
-                            } else {
-                                res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
-                            }
+                            const res = await (async () => {
+                                if (this._regenInput) {
+                                    const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+                                    this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+                                    return gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+                                }
+                                return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+                            })();
                             if (res) {
                                 const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
                                 this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc);
-- 
cgit v1.2.3-70-g09d2


From 215ad40efa2e343e290d18bffbc55884829f1a0d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 4 Mar 2025 00:52:53 -0500
Subject: fixed up smartDrawHandler a bit to support svg's better.  you can now
 drop in a .svg file from the filesystem - still some unfinished business
 (arcs,  background/foreground color inversion)

---
 src/client/documents/DocUtils.ts                   |  57 +++-
 src/client/util/bezierFit.ts                       | 302 +++++++++++++++++----
 src/client/views/collections/CollectionSubView.tsx |  17 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  36 +--
 src/client/views/pdf/AnchorMenu.tsx                |   2 +-
 src/client/views/smartdraw/SmartDrawHandler.tsx    |  72 +++--
 6 files changed, 363 insertions(+), 123 deletions(-)

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

diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 18c8d97d4..7f22e9376 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -10,7 +10,7 @@ import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, FieldResult, FieldType, LinkedTo, Opt, StrListCast } from '../../fields/Doc';
 import { DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
-import { InkDataFieldName, InkField } from '../../fields/InkField';
+import { InkData, InkDataFieldName, InkField } from '../../fields/InkField';
 import { List, ListFieldName } from '../../fields/List';
 import { ProxyField } from '../../fields/Proxy';
 import { RichTextField } from '../../fields/RichTextField';
@@ -33,6 +33,10 @@ import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
 import { DocumentType } from './DocumentTypes';
 import { Docs, DocumentOptions } from './Documents';
 import { DocumentView } from '../views/nodes/DocumentView';
+import { INode, parse } from 'svgson';
+import { SVGToBezier, SVGType } from '../util/bezierFit';
+import { SmartDrawHandler } from '../views/smartdraw/SmartDrawHandler';
+import { PointData } from '../../pen-gestures/GestureTypes';
 
 export namespace DocUtils {
     function HasFunctionFilter(val: string) {
@@ -780,6 +784,57 @@ export namespace DocUtils {
         return generatedDocuments;
     }
 
+    export async function openSVGfile(file: File, options: DocumentOptions) {
+        const reader = new FileReader();
+        const scale = 1;
+        const startPoint = { X: (options.x as number) ?? 0, Y: (options.y as number) ?? 0 };
+        const buffer = await new Promise<string>((res, rej) => {
+            reader.onload = event => {
+                const fileContent = event.target?.result;
+                // Process the file content here
+                console.log(fileContent);
+                typeof fileContent === 'string' ? res(fileContent) : rej();
+            };
+
+            reader.readAsText(file);
+        });
+        const svg = buffer.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+        if (svg) {
+            const svgObject = await parse(svg[0]);
+            const strokeData: [InkData, string, string][] = [];
+            const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER };
+            let last: PointData = { X: 0, Y: 0 };
+            const processStroke = (child: INode) => {
+                child.attributes.d
+                    .split(/[\n]?M/)
+                    .slice(1)
+                    .map((d, ind) => {
+                        const convertedBezier: InkData = SVGToBezier(child.name as SVGType, { ...child, d: '\nM' + d } as unknown as Record<string, string>, last);
+                        last = convertedBezier.lastElement();
+                        convertedBezier.forEach(point => {
+                            if (point.X < tl.X) tl.X = point.X;
+                            if (point.Y < tl.Y) tl.Y = point.Y;
+                        });
+                        strokeData.push([convertedBezier, child.attributes.stroke || 'black', ind === 0 ? child.attributes.fill : child.attributes.fill === 'none' ? child.attributes.fill : DashColor(child.attributes.fill).negate().toString()]);
+                    });
+            };
+            const processNode = (parent: INode) => {
+                if (parent.children.length) parent.children.forEach(processNode);
+                else if (parent.type !== 'text') processStroke(parent);
+            };
+            processNode(svgObject);
+
+            const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * scale, Y: startPoint.Y + (pd.Y - tl.Y) * scale });
+
+            return SmartDrawHandler.CreateDrawingDoc(
+                strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as [PointData[], string, string]),
+                { autoColor: true },
+                '',
+                undefined
+            );
+        }
+    }
+
     export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
         const generatedDocuments: Doc[] = [];
         // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid.
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index 7ef370d48..65bd44bf9 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -1,7 +1,5 @@
 /* eslint-disable no-use-before-define */
-/* eslint-disable prefer-destructuring */
 /* eslint-disable no-param-reassign */
-/* eslint-disable camelcase */
 import { Point } from '../../pen-gestures/ndollar';
 
 export enum SVGType {
@@ -625,13 +623,130 @@ export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
     return [...firstEnd, ...points, ...lastEnd];
 }
 
-export function SVGToBezier(name: SVGType, attributes: any): Point[] {
+function convertToAbsolute(pathData: string): string {
+    const commands = pathData.match(/[a-zA-Z][^a-zA-Z]*/g);
+    if (!commands) return pathData;
+
+    let currentX = 0;
+    let currentY = 0;
+    let startX = 0;
+    let startY = 0;
+
+    const absoluteCommands = commands.map(command => {
+        const type = command[0];
+        const values = command
+            .slice(1)
+            .trim()
+            .split(/[\s,]+/)
+            .map(v => +v);
+
+        switch (type) {
+            case 'M':
+                currentX = values[0];
+                currentY = values[1];
+                startX = currentX;
+                startY = currentY;
+                return `M${currentX},${currentY}`;
+            case 'm':
+                currentX += values[0];
+                currentY += values[1];
+                startX = currentX;
+                startY = currentY;
+                return `M${currentX},${currentY}`;
+            case 'L':
+                currentX = values[0];
+                currentY = values[1];
+                return `L${currentX},${currentY}`;
+            case 'l':
+                currentX += values[0];
+                currentY += values[1];
+                return `L${currentX},${currentY}`;
+            case 'H':
+                currentX = values[0];
+                return `H${currentX}`;
+            case 'h':
+                currentX += values[0];
+                return `H${currentX}`;
+            case 'V':
+                currentY = values[0];
+                return `V${currentY}`;
+            case 'v':
+                currentY += values[0];
+                return `V${currentY}`;
+            case 'C':
+                currentX = values[4];
+                currentY = values[5];
+                return `C${values.join(',')}`;
+            case 'c': {
+                let str = '';
+                for (let i = 0; i < values.length; i += 6) {
+                    str += (i === 0 ? 'C':',') + (values[i] + currentX) + 
+                                           ',' + (values[i + 1] + currentY) + 
+                                           ',' + (values[i + 2] + currentX) + 
+                                           ',' + (values[i + 3] + currentY) + 
+                                           ',' + (values[i + 4] + currentX) + 
+                                           ',' + (values[i + 5] + currentY); // prettier-ignore
+                    currentX += values[i + 4];
+                    currentY += values[i + 5];
+                }
+                return str;
+            }
+            case 'S':
+                currentX = values[2];
+                currentY = values[3];
+                return `S${values.join(',')}`;
+            case 's':
+                return `S${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`;
+            case 'Q':
+                currentX = values[2];
+                currentY = values[3];
+                return `Q${values.join(',')}`;
+            case 'q': {
+                let str = '';
+                for (let i = 0; i < values.length; i += 4) {
+                    str += (i === 0 ? 'Q':',') + (values[i] + currentX) + 
+                                           ',' + (values[i + 1] + currentY) + 
+                                           ',' + (values[i + 2] + currentX) + 
+                                           ',' + (values[i + 3] + currentY); // prettier-ignore
+                    currentX += values[i + 2];
+                    currentY += values[i + 3];
+                }
+                return str;
+            }
+            case 'T':
+                currentX = values[0];
+                currentY = values[1];
+                return `T${currentX},${currentY}`;
+            case 't':
+                currentX += values[0];
+                currentY += values[1];
+                return `T${currentX},${currentY}`;
+            case 'A':
+                currentX = values[5];
+                currentY = values[6];
+                return `A${values.join(',')}`;
+            case 'a':
+                return `A${values.map((v, i) => (i % 2 === 0 ? (currentX += v) : (currentY += v))).join(',')}`;
+            case 'Z':
+            case 'z':
+                currentX = startX;
+                currentY = startY;
+                return 'Z';
+            default:
+                return command;
+        }
+    });
+
+    return absoluteCommands.join(' ');
+}
+
+export function SVGToBezier(name: SVGType, attributes: Record<string, string>, last: { X: number; Y: number }): Point[] {
     switch (name) {
         case 'line': {
-            const x1 = parseInt(attributes.x1);
-            const x2 = parseInt(attributes.x2);
-            const y1 = parseInt(attributes.y1);
-            const y2 = parseInt(attributes.y2);
+            const x1 = +attributes.x1;
+            const x2 = +attributes.x2;
+            const y1 = +attributes.y1;
+            const y2 = +attributes.y2;
             return [
                 { X: x1, Y: y1 },
                 { X: x1, Y: y1 },
@@ -642,10 +757,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
         case 'circle':
         case 'ellipse': {
             const c = 0.551915024494;
-            const centerX = parseInt(attributes.cx);
-            const centerY = parseInt(attributes.cy);
-            const radiusX = parseInt(attributes.rx) || parseInt(attributes.r);
-            const radiusY = parseInt(attributes.ry) || parseInt(attributes.r);
+            const centerX = +attributes.cx;
+            const centerY = +attributes.cy;
+            const radiusX = +attributes.rx || +attributes.r;
+            const radiusY = +attributes.ry || +attributes.r;
             return [
                 { X: centerX, Y: centerY + radiusY },
                 { X: centerX + c * radiusX, Y: centerY + radiusY },
@@ -666,10 +781,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             ];
         }
         case 'rect': {
-            const x = parseInt(attributes.x);
-            const y = parseInt(attributes.y);
-            const width = parseInt(attributes.width);
-            const height = parseInt(attributes.height);
+            const x = +attributes.x;
+            const y = +attributes.y;
+            const width = +attributes.width;
+            const height = +attributes.height;
             return [
                 { X: x, Y: y },
                 { X: x, Y: y },
@@ -691,41 +806,122 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
         }
         case 'path': {
             const coordList: Point[] = [];
-            const [startX, startY] = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/).slice(1);
-            const startPt = { X: parseInt(startX), Y: parseInt(startY) };
-            coordList.push(startPt);
-            const matches: RegExpMatchArray[] = Array.from(
-                attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
-            );
-            let lastPt: Point = startPt;
-            matches.forEach(match => {
-                if (match[0].startsWith('Q')) {
-                    coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
-                    coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
-                    coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
-                    coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
-                    lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) };
-                } else if (match[0].startsWith('C')) {
-                    coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) });
-                    coordList.push({ X: parseInt(match[7]), Y: parseInt(match[8]) });
-                    coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
-                    coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
-                    lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) };
-                } else {
-                    coordList.push(lastPt);
-                    coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
-                    coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
-                    coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
-                    lastPt = { X: parseInt(match[11]), Y: parseInt(match[12]) };
+            let fixedattrs = attributes.d.trim().replace(/([0-9])-/g, '$1,-');
+            for (let i = 0; i < 100; i++) {
+                const test = fixedattrs.replace(/([0-9]?\.[0-9]+)(\.[0-9]+)/g, '$1,$2');
+                if (test === fixedattrs) break;
+                fixedattrs = test;
+            }
+            const attrdata = convertToAbsolute(fixedattrs);
+            const move = attrdata.match(/M(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+            const [startX, startY] = move?.slice(1) ?? [last.X + '', last.Y + ''];
+            const startPt = { X: +startX, Y: +startY };
+            let first = true;
+            let lastCmd = '';
+            for (let attr = attrdata.slice(move?.[0].length ?? 0).trim(); attr; ) {
+                lastCmd = 'AQCLVHZ'.includes(attr[0]) ? attr[0] : lastCmd;
+                switch (lastCmd) {
+                    case 'Q': {
+                        const match = attr.match(/Q?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+                        if (match) {
+                            const prev = first ? startPt : coordList.lastElement();
+                            const Q = [+match[1], +match[2], +match[3], +match[4]];
+
+                            coordList.push(prev);
+                            coordList.push({ X: prev.X + (2 / 3) * (Q[0] - prev.X), Y: prev.Y + (2 / 3) * (Q[1] - prev.Y) });
+                            coordList.push({ X: Q[2] + (2 / 3) * (Q[0] - Q[2]), Y: Q[3] + (2 / 3) * (Q[1] - Q[3]) });
+                            coordList.push({ X: Q[2], Y: Q[3] });
+                            attr = attr.slice(match[0].length).trim();
+                        } else {
+                            attr = attr.slice(1).trim();
+                            alert('error' + attr);
+                        }
+                        break;
+                    }
+                    case 'C': {
+                        const match = attr.match(/C?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+                        if (match) {
+                            coordList.push(first ? startPt : coordList.lastElement());
+                            coordList.push({ X: +match[1], Y: +match[2] });
+                            coordList.push({ X: +match[3], Y: +match[4] });
+                            coordList.push({ X: +match[5], Y: +match[6] });
+                            attr = attr.slice(match[0].length).trim();
+                        } else {
+                            attr = attr.slice(1).trim();
+                            alert('error' + attr);
+                        }
+                        break;
+                    }
+                    case 'A': {
+                        const match = attr.match(/A?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+                        if (match) {
+                            console.log('SKIPPING arc - not implemented');
+                            // coordList.push(first ? startPt : coordList.lastElement());
+                            // coordList.push({ X: +match[1], Y: +match[2] });
+                            // coordList.push({ X: +match[3], Y: +match[4] });
+                            // coordList.push({ X: +match[5], Y: +match[6] });
+                            attr = attr.slice(match[0].length).trim();
+                        } else {
+                            attr = attr.slice(1).trim();
+                            alert('error' + attr);
+                        }
+                        break;
+                    }
+                    case 'L': {
+                        const match = attr.match(/L?[, ]?(-?\d*\.?\d*)[, ]?(-?\d*\.?\d*)/);
+                        if (match) {
+                            coordList.push(first ? startPt : coordList.lastElement());
+                            coordList.push(coordList.lastElement());
+                            coordList.push({ X: +match[1], Y: +match[2] });
+                            coordList.push({ X: +match[1], Y: +match[2] });
+                            attr = attr.slice(match[0].length).trim();
+                        } else {
+                            attr = attr.slice(1).trim();
+                            alert('error' + attr);
+                        }
+                        break;
+                    }
+                    case 'H': {
+                        const match = attr.match(/H?[, ]?(-?\d*\.?\d*)/);
+                        if (match) {
+                            coordList.push(first ? startPt : coordList.lastElement());
+                            coordList.push(coordList.lastElement());
+                            coordList.push({ X: +match[1], Y: coordList.lastElement().Y });
+                            coordList.push({ X: +match[1], Y: coordList.lastElement().Y });
+                            attr = attr.slice(match[0].length).trim();
+                        } else {
+                            attr = attr.slice(1).trim();
+                            alert('error' + attr);
+                        }
+                        break;
+                    }
+                    case 'V': {
+                        const match = attr.match(/V?[, ]?(-?\d*\.?\d*)/);
+                        if (match) {
+                            coordList.push(first ? startPt : coordList.lastElement());
+                            coordList.push(coordList.lastElement());
+                            coordList.push({ X: coordList.lastElement().X, Y: +match[1] });
+                            coordList.push({ X: coordList.lastElement().X, Y: +match[1] });
+                            attr = attr.slice(match[0].length).trim();
+                        } else {
+                            attr = attr.slice(1).trim();
+                            alert('error' + attr);
+                        }
+                        break;
+                    }
+                    case 'Z': {
+                        coordList.push(first ? startPt : coordList.lastElement());
+                        coordList.push(first ? startPt : coordList.lastElement());
+                        coordList.push(startPt);
+                        coordList.push(startPt);
+                        attr = attr.slice(1).trim();
+                        break;
+                    }
+                    default:
+                        attr = attr.slice(1).trim();
+                        debugger;
                 }
-            });
-            const hasZ = attributes.d.match(/Z/);
-            if (hasZ || attributes.fill) {
-                coordList.push(lastPt);
-                coordList.push(startPt);
-                coordList.push(startPt);
-            } else {
-                coordList.pop();
+                first = false;
             }
             return coordList;
         }
@@ -733,10 +929,10 @@ export function SVGToBezier(name: SVGType, attributes: any): Point[] {
             const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g));
             let list: Point[] = [];
             coords.forEach(coord => {
-                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
-                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
-                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
-                list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) });
+                list.push({ X: +coord[1], Y: +coord[2] });
+                list.push({ X: +coord[1], Y: +coord[2] });
+                list.push({ X: +coord[1], Y: +coord[2] });
+                list.push({ X: +coord[1], Y: +coord[2] });
             });
             const firstPts = list.splice(0, 2);
             list = list.concat(firstPts);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index ca830aa6f..655894e40 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -541,12 +541,17 @@ export function CollectionSubView<X>() {
                 DocUtils.uploadYoutubeVideoLoading(files, {}, loading);
             } else {
                 generatedDocuments.push(
-                    ...files.map(file => {
-                        const loading = Docs.Create.LoadingDocument(file, options);
-                        Doc.addCurrentlyLoading(loading);
-                        DocUtils.uploadFileToDoc(file, {}, loading);
-                        return loading;
-                    })
+                    ...(await Promise.all(
+                        files.map(async file => {
+                            if (file.name.endsWith('svg')) {
+                                return (await DocUtils.openSVGfile(file, options)) as Doc;
+                            }
+                            const loading = Docs.Create.LoadingDocument(file, options);
+                            Doc.addCurrentlyLoading(loading);
+                            DocUtils.uploadFileToDoc(file, {}, loading);
+                            return loading;
+                        })
+                    ))
                 );
             }
             if (generatedDocuments.length) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aa9b9b0fa..b3d908da4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -30,7 +30,7 @@ import { CompileScript } from '../../../util/Scripting';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
-import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
+import { undoable, UndoManager } from '../../../util/UndoManager';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
 import { InkingStroke } from '../../InkingStroke';
@@ -1232,7 +1232,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     showSmartDraw = (x: number, y: number, regenerate?: boolean) => {
         const sm = SmartDrawHandler.Instance;
-        sm.CreateDrawingDoc = this.createDrawingDoc;
         sm.RemoveDrawing = this.removeDrawing;
         sm.AddDrawing = this.addDrawing;
         (regenerate ? sm.displayRegenerate : sm.displaySmartDrawHandler)(x, y, NumCast(this.layoutDoc[this.scaleFieldKey]));
@@ -1240,38 +1239,6 @@ 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) => {
-        this._drawing = [];
-        const xf = this.screenToFreeformContentsXf;
-        strokeData.forEach((stroke: [InkData, string, string]) => {
-            const bounds = InkField.getBounds(stroke[0]);
-            const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
-            const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
-            const inkDoc = Docs.Create.InkDocument(
-                stroke[0],
-                { 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().activeHideTextLabels)}, // prettier-ignore
-                inkWidth,
-                opts.autoColor ? stroke[1] : ActiveInkColor(),
-                ActiveInkBezierApprox(),
-                stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
-                ActiveInkArrowStart(),
-                ActiveInkArrowEnd(),
-                ActiveInkDash(),
-                ActiveIsInkMask()
-            );
-            this._drawing.push(inkDoc);
-        });
-        return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 });
-    };
 
     /**
      * Part of regenerating a drawing--deletes the old drawing.
@@ -2007,7 +1974,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             optionItems.push({
                 description: 'Regenerate AI Drawing',
                 event: action(() => {
-                    SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
                     SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
                     SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
                     !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index f7070c780..28371594e 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -139,7 +139,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
     createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
         this.AddDrawingAnnotation(drawing);
         const docData = drawing[DocData];
-        docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+        docData.title = opts.text?.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
         docData.ai_drawing_input = opts.text;
         docData.ai_drawing_complexity = opts.complexity;
         docData.ai_drawing_colored = opts.autoColor;
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
index fbf471900..ca308015d 100644
--- a/src/client/views/smartdraw/SmartDrawHandler.tsx
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -27,16 +27,19 @@ import { ActiveInkArrowEnd, ActiveInkArrowStart, ActiveInkBezierApprox, ActiveIn
 import { FireflyDimensionsMap, FireflyImageData, FireflyImageDimensions } from './FireflyConstants';
 import './SmartDrawHandler.scss';
 import { Upload } from '../../../server/SharedMediaTypes';
+import { PointData } from '../../../pen-gestures/GestureTypes';
 
 export interface DrawingOptions {
-    text: string;
-    complexity: number;
-    size: number;
-    autoColor: boolean;
-    x: number;
-    y: number;
+    text?: string;
+    complexity?: number;
+    size?: number;
+    autoColor?: boolean;
+    x?: number;
+    y?: number;
 }
 
+type svgparsedData = [PointData[], string, string];
+
 /**
  * The SmartDrawHandler allows users to generate drawings with GPT from text input. Users are able to enter
  * the item to draw, how complex they want the drawing to be, how large the drawing should be, and whether
@@ -102,7 +105,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of
      * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions.
      */
-    public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
+    public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
         const drawing: Doc[] = [];
         strokeList.forEach((stroke: [InkData, string, string]) => {
             const bounds = InkField.getBounds(stroke[0]);
@@ -114,7 +117,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         y: bounds.top - inkWidth / 2,
                         _width: bounds.width + inkWidth,
                         _height: bounds.height + inkWidth,
-                        stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
+                        stroke_showLabel: false}, // prettier-ignore
                 inkWidth,
                 opts.autoColor ? stroke[1] : ActiveInkColor(),
                 ActiveInkBezierApprox(),
@@ -188,9 +191,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     /**
      * This allows users to press the return/enter key to send input.
      */
-    handleKeyPress = (event: React.KeyboardEvent) => {
-        if (event.key === 'Enter') {
-            this.handleSendClick();
+    handleKeyPress = (e: React.KeyboardEvent) => {
+        if (e.key === 'Enter') {
+            this.handleSendClick(this._pageX, this._pageY);
         }
     };
 
@@ -200,7 +203,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      * what the user sees.
      */
     @action
-    handleSendClick = async () => {
+    handleSendClick = async (X: number, Y: number) => {
         if ((!this.ShowRegenerate && this._userInput == '') || (!this._generateImage && !this._generateDrawing)) return;
         this._isLoading = true;
         this._canInteract = false;
@@ -213,7 +216,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     await this.createImageWithFirefly(this._userInput);
                 }
                 if (this._generateDrawing) {
-                    await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+                    await this.drawWithGPT({ X, Y }, this._userInput, this._complexity, this._size, this._autoColor);
                 }
                 this.hideSmartDrawHandler();
             } catch (err) {
@@ -229,14 +232,14 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
     /**
      * Calls GPT API to create a drawing based on user input.
      */
-    drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
+    drawWithGPT = async (screenPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => {
         if (input) {
-            this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y };
+            this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: screenPt.X, y: screenPt.Y };
             const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
             if (res) {
-                const strokeData = await this.parseSvg(res, startPt, false, autoColor);
-                const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
-                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+                const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, false, autoColor);
+                const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+                drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res, screenPt.X, screenPt.Y);
                 drawingDoc && this._selectedDocs.push(drawingDoc);
                 return strokeData;
             } else {
@@ -334,9 +337,9 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                                 return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
                             })();
                             if (res) {
-                                const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+                                const strokeData = await this.parseSvg(res, { X: this._lastInput.x ?? 0, Y: this._lastInput.y ?? 0 }, true, lastInput?.autoColor || this._autoColor);
                                 this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc);
-                                const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+                                const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
                                 drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
                             } else {
                                 console.error('GPT call failed');
@@ -356,20 +359,35 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
      */
     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];
             const svgObject = await parse(svg[0]);
+            console.log(res, svgObject);
             const svgStrokes: INode[] = svgObject.children;
             const strokeData: [InkData, string, string][] = [];
+
+            const tl = { X: Number.MAX_SAFE_INTEGER, Y: Number.MAX_SAFE_INTEGER };
+            let last: PointData = { X: 0, Y: 0 };
             svgStrokes.forEach(child => {
-                const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
+                const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes, last);
+                last = convertedBezier.lastElement();
                 strokeData.push([
-                    convertedBezier.map(point => ({ X: startPoint.X + (point.X - startPoint.X) * this._scale, Y: startPoint.Y + (point.Y - startPoint.Y) * this._scale })),
+                    convertedBezier.map(point => {
+                        if (point.X < tl.X) tl.X = point.X;
+                        if (point.Y < tl.Y) tl.Y = point.Y;
+                        return { X: point.X, Y: point.Y };
+                    }),
                     (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : '',
                     (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
                 ]);
             });
-            return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
+            const mapStroke = (pd: PointData): PointData => ({ X: startPoint.X + (pd.X - tl.X) * this._scale, Y: startPoint.Y + (pd.Y - tl.Y) * this._scale });
+            return {
+                data: strokeData.map(sdata => [sdata[0].map(mapStroke), sdata[1], sdata[2]] as svgparsedData),
+                lastInput: this._lastInput,
+                lastRes: svg[0],
+            };
         }
     };
 
@@ -439,7 +457,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         },
                     }}
                     checked={this._generateImage}
-                    onChange={() => this._canInteract && (this._generateImage = !this._generateImage)}
+                    onChange={action(() => this._canInteract && (this._generateImage = !this._generateImage))}
                 />
             </div>
         </div>
@@ -566,7 +584,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                         icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                         iconPlacement="right"
                         color={SettingsManager.userColor}
-                        onClick={this.handleSendClick}
+                        onClick={() => this.handleSendClick(this._pageX, this._pageY)}
                     />
                 </div>
                 {this._showOptions && (
@@ -598,7 +616,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                 icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
                 iconPlacement="right"
                 color={SettingsManager.userColor}
-                onClick={this.handleSendClick}
+                onClick={() => this.handleSendClick(this._pageX, this._pageY)}
             />
         </div>
     );
@@ -644,7 +662,7 @@ export class SmartDrawHandler extends ObservableReactComponent<object> {
                     tooltip="Regenerate"
                     icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
                     color={SettingsManager.userColor}
-                    onClick={this.handleSendClick}
+                    onClick={() => this.handleSendClick(this._pageX, this._pageY)}
                 />
                 <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={action(() => (this._showEditBox = !this._showEditBox))} />
                 {this._showEditBox ? this.renderRegenerateEditBox() : null}
-- 
cgit v1.2.3-70-g09d2

