From 4c768162e0436115a05b9c8b0e4d837d626d45ba Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 30 Oct 2024 18:54:52 -0400
Subject: reworked how context menu buttons for ink and text work. added
 disableMixBlend for making transparent docs not use 'multiply'.

---
 src/client/views/nodes/WebBox.tsx | 13 +++++--------
 1 file changed, 5 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a5788d02a..9ba4f8ead 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -44,6 +44,7 @@ import { LinkInfo } from './LinkDocPreview';
 import { OpenWhere } from './OpenWhere';
 import './WebBox.scss';
 
+// eslint-disable-next-line @typescript-eslint/no-require-imports
 const { CreateImage } = require('./WebBoxRenderer');
 
 @observer
@@ -335,7 +336,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 ele = document.createElement('div');
                 ele.append(contents);
             }
-        } catch (e) {
+        } catch {
             /* empty */
         }
         const visibleAnchor = this._getAnchor(this._savedAnnotations, true);
@@ -506,7 +507,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         let href: Opt<string>;
         try {
             href = iframe?.contentWindow?.location.href;
-        } catch (e) {
+        } catch {
             runInAction(() => this._warning++);
             href = undefined;
         }
@@ -713,7 +714,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     this._webUrl = this._url;
                 }
             }
-        } catch (e) {
+        } catch {
             console.log('WebBox URL error:' + this._url);
         }
         return true;
@@ -805,7 +806,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             sel.empty(); // Chrome
         else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
         this.marqueeing = [e.clientX, e.clientY];
-        if (!e.altKey && e.button === 0 && this._props.isContentActive() && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+        if (!e.altKey && e.button === 0 && this._props.isContentActive() && Doc.ActiveTool !== InkTool.Ink) {
             setupMoveUpEvents(
                 this,
                 e,
@@ -855,7 +856,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                     })}
                     contentEditable
                     onPointerDown={this.webClipDown}
-                    // eslint-disable-next-line react/no-danger
                     dangerouslySetInnerHTML={{ __html: field.html }}
                 />
             );
@@ -1031,7 +1031,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 {this.inlineTextAnnotations
                     .sort((a, b) => NumCast(a.y) - NumCast(b.y))
                     .map(anno => (
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         <Annotation {...this._props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} containerDataDoc={this.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
                     ))}
             </div>
@@ -1042,7 +1041,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     }
     renderAnnotations = (childFilters: () => string[]) => (
         <CollectionFreeFormView
-            // eslint-disable-next-line react/jsx-props-no-spreading
             {...this._props}
             setContentViewBox={this.setInnerContent}
             NativeWidth={returnZero}
@@ -1217,7 +1215,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
-                        // eslint-disable-next-line react/jsx-props-no-spreading
                         {...this._props}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         fieldKey={this.fieldKey + '_' + this._urlHash}
-- 
cgit v1.2.3-70-g09d2


From 6f8bb6d69d9e671aa6dc4b076f913a63fa73f9f2 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 6 Jan 2025 15:16:28 -0500
Subject: pdf fixes for latest version of PDFjs.

---
 src/client/views/nodes/PDFBox.tsx   |  4 ++--
 src/client/views/nodes/WebBox.tsx   |  5 +++--
 src/client/views/pdf/PDFViewer.scss | 12 ++++++++++++
 src/client/views/pdf/PDFViewer.tsx  | 11 +++++++----
 4 files changed, 24 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 2e4c87d38..37807e818 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -267,8 +267,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     @action
     loaded = (nw: number, nh: number, np: number) => {
         this.dataDoc[this._props.fieldKey + '_numPages'] = np;
-        Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72));
-        Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72);
+        Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), nw * Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS));
+        Doc.SetNativeHeight(this.dataDoc, nh * Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS);
         this.layoutDoc._height = NumCast(this.layoutDoc._width) / (Doc.NativeAspect(this.dataDoc) || 1);
         !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
     };
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 9ba4f8ead..ffeb574e9 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -202,8 +202,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             () => this.layoutDoc._layout_autoHeight,
             layoutAutoHeight => {
                 if (layoutAutoHeight) {
-                    this.layoutDoc._nativeHeight = NumCast(this.Document[this._props.fieldKey + '_nativeHeight']);
-                    this._props.setHeight?.(NumCast(this.Document[this._props.fieldKey + '_nativeHeight']) * (this._props.NativeDimScaling?.() || 1));
+                    const nh = NumCast(this.Document[this._props.fieldKey + '_nativeHeight'], NumCast(this.Document.nativeHeight));
+                    this.layoutDoc._nativeHeight = nh;
+                    this._props.setHeight?.(nh * (this._props.NativeDimScaling?.() || 1));
                 }
             }
         );
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a225c4b59..030251762 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -7,6 +7,10 @@
     left: 0;
 }
 
+:root {
+    --devicePixelRatio: 1; // the actual value of this will be set in PDFViewer.tsx;
+}
+
 .pdfViewerDash,
 .pdfViewerDash-interactive {
     position: absolute;
@@ -19,9 +23,16 @@
     overflow-x: hidden;
     transform-origin: top left;
 
+    .annotationLayer {
+        transform: scale(var(--devicePixelRatio));
+    }
     .textLayer {
         opacity: unset;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
+        transform: scale(var(--devicePixelRatio));
+    }
+    [data-main-rotation='90'] {
+        transform: scale(var(--devicePixelRatio)) rotate(90deg) translateY(-100%);
     }
     .textLayer ::selection {
         background: #accef76a;
@@ -39,6 +50,7 @@
     .page {
         position: relative;
         border: unset;
+        height: 100% !important;
     }
 
     .pdfViewerDash-text-selected {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 920c9ea8b..5594254b3 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -152,17 +152,20 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
     initialLoad = async () => {
         if (this._pageSizes.length === 0) {
             this._pageSizes = Array<{ width: number; height: number }>(this._props.pdf.numPages);
+            const devicePixelRatio = window.devicePixelRatio;
+            document.documentElement?.style.setProperty('--devicePixelRatio', devicePixelRatio.toString()); // set so that css can use this to adjust various PDFJs divs
             await Promise.all(
                 this._pageSizes.map((val, i) =>
                     this._props.pdf.getPage(i + 1).then(
                         action((page: Pdfjs.PDFPageProxy) => {
                             const page0or180 = page.rotate === 0 || page.rotate === 180;
                             this._pageSizes.splice(i, 1, {
-                                width: page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1],
-                                height: page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0],
+                                width: page.view[page0or180 ? 2 : 3] * devicePixelRatio - page.view[page0or180 ? 0 : 1] * devicePixelRatio,
+                                height: page.view[page0or180 ? 3 : 2] * devicePixelRatio - page.view[page0or180 ? 1 : 0] * devicePixelRatio,
                             });
                             if (i === this._props.pdf.numPages - 1) {
-                                this._props.loaded?.(page.view[page0or180 ? 2 : 3] - page.view[page0or180 ? 0 : 1], page.view[page0or180 ? 3 : 2] - page.view[page0or180 ? 1 : 0], this._props.pdf.numPages);
+                                const lastPage = this._pageSizes.lastElement();
+                                this._props.loaded?.(lastPage.width, lastPage.height, this._props.pdf.numPages);
                             }
                         })
                     )
@@ -170,7 +173,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
             );
         }
         runInAction(() => {
-            this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72;
+            this._scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS;
         });
     };
 
-- 
cgit v1.2.3-70-g09d2


From 93ee735e435de5949a0eec54c07537c4de2eedf4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 7 Jan 2025 01:24:55 -0500
Subject: fixed pdfs/marqueeAnnotator so that you can add rectangle
 annotations.

---
 src/client/views/MarqueeAnnotator.tsx            |  5 ++--
 src/client/views/nodes/DataVizBox/DataVizBox.tsx |  1 +
 src/client/views/nodes/ImageBox.tsx              |  6 ++---
 src/client/views/nodes/PDFBox.scss               | 11 ++++++++
 src/client/views/nodes/PDFBox.tsx                | 34 +++++++++++-------------
 src/client/views/nodes/VideoBox.tsx              |  1 +
 src/client/views/nodes/WebBox.tsx                |  1 +
 src/client/views/pdf/PDFViewer.tsx               | 25 +++++++++--------
 8 files changed, 48 insertions(+), 36 deletions(-)

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

diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index fa1123a2d..02516264c 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -15,18 +15,19 @@ import './MarqueeAnnotator.scss';
 import { DocumentView } from './nodes/DocumentView';
 import { ObservableReactComponent } from './ObservableReactComponent';
 import { AnchorMenu } from './pdf/AnchorMenu';
+import { Transform } from '../util/Transform';
 
 export interface MarqueeAnnotatorProps {
     Document: Doc;
     down?: number[];
     scrollTop: number;
-    isNativeScaled?: boolean;
     scaling?: () => number;
     annotationLayerScaling?: () => number;
     annotationLayerScrollTop: number;
     containerOffset?: () => number[];
     marqueeContainer: HTMLDivElement;
     docView: () => DocumentView;
+    screenTransform: () => Transform;
     savedAnnotations: () => ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>;
     selectionText: () => string;
     annotationLayer: HTMLDivElement;
@@ -157,7 +158,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
     //    4) reattach the vector to the center of the bounding box
     getTransformedScreenPt = (down: number[]) => {
         const { marqueeContainer } = this.props;
-        const containerXf = this.props.isNativeScaled ? this.props.docView().screenToContentsTransform() : this.props.docView().screenToViewTransform();
+        const containerXf = this.props.screenTransform();
         const boundingRect = marqueeContainer.getBoundingClientRect();
         const center = { x: boundingRect.x + boundingRect.width / 2, y: boundingRect.y + boundingRect.height / 2 };
         const downVec = Utils.rotPt(down[0] - center.x,  
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 7026c3b01..b874d077b 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -832,6 +832,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         annotationLayerScrollTop={NumCast(this.Document._layout_scrollTop)}
                         scaling={returnOne}
                         docView={this.DocumentView}
+                        screenTransform={this.DocumentView().screenToViewTransform}
                         addDocument={this.sidebarAddDocument}
                         finishMarquee={this.finishMarquee}
                         savedAnnotations={this.savedAnnotations}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 423a73f44..de8a9ad21 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -39,7 +39,6 @@ import { FocusViewOptions } from './FocusViewOptions';
 import './ImageBox.scss';
 import { OpenWhere } from './OpenWhere';
 import { Upload } from '../../../server/SharedMediaTypes';
-import { ImageUtils } from '../../util/Import & Export/ImageUtils';
 
 export class ImageEditorData {
     // eslint-disable-next-line no-use-before-define
@@ -268,7 +267,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const anchy = NumCast(cropping.y);
         const anchw = NumCast(cropping._width);
         const anchh = NumCast(cropping._height);
-        const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) / anchw;
+        const viewScale = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) / anchh;
         cropping.title = 'crop: ' + this.Document.title;
         cropping.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width);
         cropping.y = NumCast(this.Document.y);
@@ -286,9 +285,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         croppingProto.data_nativeWidth = anchw;
         croppingProto.data_nativeHeight = anchh;
         croppingProto.freeform_scale = viewScale;
-        croppingProto.freeform_scale_min = viewScale;
         croppingProto.freeform_panX = anchx / viewScale;
         croppingProto.freeform_panY = anchy / viewScale;
+        croppingProto.freeform_scale_min = viewScale;
         croppingProto.freeform_panX_min = anchx / viewScale;
         croppingProto.freeform_panX_max = anchw / viewScale;
         croppingProto.freeform_panY_min = anchy / viewScale;
@@ -593,6 +592,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         annotationLayerScrollTop={0}
                         scaling={returnOne}
                         annotationLayerScaling={this._props.NativeDimScaling}
+                        screenTransform={this.DocumentView().screenToViewTransform}
                         docView={this.DocumentView}
                         addDocument={this.addDocument}
                         finishMarquee={this.finishMarquee}
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 7bca1230f..f6908d5fd 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -250,6 +250,17 @@
     cursor: ew-resize;
     background: lightGray;
 }
+.pdfBox-container {
+    position: absolute;
+    transform-origin: top left;
+    top: 0;
+}
+.pdfBox-sidebarContainer {
+    position: absolute;
+    height: 100%;
+    right: 0;
+    top: 0;
+}
 
 .pdfBox-interactive {
     width: 100%;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 1de4d2512..06b75e243 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
@@ -40,8 +40,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(PDFBox, fieldKey);
     }
+    static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
+    static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
     public static openSidebarWidth = 250;
     public static sidebarResizerWidth = 5;
+
     private _searchString: string = '';
     private _initialScrollTarget: Opt<Doc>;
     private _pdfViewer: PDFViewer | undefined;
@@ -63,11 +66,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
         !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
         if (this.pdfUrl) {
-            if (PDFBox.pdfcache.get(this.pdfUrl.url.href))
-                runInAction(() => {
-                    this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href);
-                });
-            else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href))
+            this._pdf = PDFBox.pdfcache.get(this.pdfUrl.url.href);
+            !this._pdf &&
                 PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(
                     action(pdf => {
                         this._pdf = pdf;
@@ -265,12 +265,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     @action
-    loaded = (nw: number, nh: number, np: number) => {
-        this.dataDoc[this._props.fieldKey + '_numPages'] = np;
-        Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), nw));
-        Doc.SetNativeHeight(this.dataDoc, nh);
+    loaded = (p: { width: number; height: number }, pages: number) => {
+        this.dataDoc[this._props.fieldKey + '_numPages'] = pages;
+        Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), p.width));
+        Doc.SetNativeHeight(this.dataDoc, p.height);
         this.layoutDoc._height = NumCast(this.layoutDoc._width) / (Doc.NativeAspect(this.dataDoc) || 1);
-        !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (nh / nw));
+        !this.Document._layout_fitWidth && (this.Document._height = NumCast(this.Document._width) * (p.height / p.width));
     };
 
     override search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
@@ -596,13 +596,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 }}>
                 <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
                 <div
+                    className="pdfBox-container"
                     style={{
                         width: `calc(${100 / viewScale}% - ${(this.sidebarWidth() / viewScale) * (this._previewWidth ? viewScale : 1)}px)`,
                         height: `${100 / viewScale}%`,
                         transform: `scale(${viewScale})`,
-                        position: 'absolute',
-                        transformOrigin: 'top left',
-                        top: 0,
                     }}>
                     <PDFViewer
                         {...this._props}
@@ -615,7 +613,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         focus={this.focus}
                         url={this.pdfUrl!.url.pathname}
                         anchorMenuClick={this.anchorMenuClick}
-                        loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
+                        loaded={Doc.NativeAspect(this.dataDoc) ? emptyFunction : this.loaded}
                         setPdfViewer={this.setPdfViewer}
                         addDocument={this.addDocument}
                         moveDocument={this.moveDocument}
@@ -624,14 +622,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                         crop={this.crop}
                     />
                 </div>
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
+                <div className="pdfBox-sidebarContainer" style={{ width: `calc(100 * ${this.sidebarWidth() / this._props.PanelWidth()}%` }}>
+                    {this.sidebarCollection}
+                </div>
                 {this.settingsPanel()}
             </div>
         );
     }
 
-    static pdfcache = new Map<string, Pdfjs.PDFDocumentProxy>();
-    static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
     render() {
         TraceMobx();
         const pdfView = !this._pdf ? null : this.renderPdfView;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3bf7de2fe..9adee53e8 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1023,6 +1023,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             scaling={returnOne}
                             annotationLayerScaling={this._props.NativeDimScaling}
                             docView={this.DocumentView}
+                            screenTransform={this.DocumentView().screenToViewTransform}
                             containerOffset={this.marqueeOffset}
                             addDocument={this.addDocWithTimecode}
                             finishMarquee={this.finishMarquee}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index ffeb574e9..6026d9ca7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1196,6 +1196,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                             scaling={this._props.NativeDimScaling}
                             addDocument={this.addDocumentWrapper}
                             docView={this.DocumentView}
+                            screenTransform={this.DocumentView().screenToViewTransform}
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotationsCreator}
                             selectionText={this.selectionText}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 722492e8d..ca7f72811 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -30,6 +30,7 @@ import { GPTPopup } from './GPTPopup/GPTPopup';
 import './PDFViewer.scss';
 import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
 import ReactLoading from 'react-loading';
+import { Transform } from '../../util/Transform';
 
 interface IViewerProps extends FieldViewProps {
     pdfBox: PDFBox;
@@ -40,7 +41,7 @@ interface IViewerProps extends FieldViewProps {
     pdf: Pdfjs.PDFDocumentProxy;
     url: string;
     sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean;
-    loaded?: (nw: number, nh: number, np: number) => void;
+    loaded: (p: { width: number; height: number }, pages: number) => void;
     // eslint-disable-next-line no-use-before-define
     setPdfViewer: (view: PDFViewer) => void;
     anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
@@ -151,24 +152,21 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
     }
 
     initialLoad = () => {
+        const page0or180 = (page: { rotate: number }) => page.rotate === 0 || page.rotate === 180;
         if (this._pageSizes.length === 0) {
             const devicePixelRatio = window.devicePixelRatio;
             document.documentElement?.style.setProperty('--devicePixelRatio', window.devicePixelRatio.toString()); // set so that css can use this to adjust various PDFJs divs
             Promise.all(
                 numberRange(this._props.pdf.numPages).map(i =>
-                    this._props.pdf.getPage(i + 1).then(page => {
-                        const page0or180 = page.rotate === 0 || page.rotate === 180;
-                        return {
-                            width: page.view[page0or180 ? 2 : 3] * devicePixelRatio - page.view[page0or180 ? 0 : 1] * devicePixelRatio,
-                            height: page.view[page0or180 ? 3 : 2] * devicePixelRatio - page.view[page0or180 ? 1 : 0] * devicePixelRatio,
-                        };
-                    })
+                    this._props.pdf.getPage(i + 1).then(page => ({
+                        width: (page.view[page0or180(page) ? 2 : 3] - page.view[page0or180(page) ? 0 : 1]) * devicePixelRatio,
+                        height: (page.view[page0or180(page) ? 3 : 2] - page.view[page0or180(page) ? 1 : 0]) * devicePixelRatio,
+                    }))
                 )
             ).then(
                 action(pages => {
                     this._pageSizes = pages;
-                    const lastPage = pages.lastElement();
-                    this._props.loaded?.(lastPage.width, lastPage.height, this._props.pdf.numPages);
+                    this._props.loaded(pages.lastElement(), this._props.pdf.numPages);
                     this.createPdfViewer();
                 })
             );
@@ -599,6 +597,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
     }
     savedAnnotations = () => this._savedAnnotations;
     addDocumentWrapper = (doc: Doc | Doc[]) => this._props.addDocument!(doc);
+    screenToMarqueeXf = () => this.props.pdfBox.DocumentView?.()?.screenToContentsTransform().scale(Pdfjs.PixelsPerInch.PDF_TO_CSS_UNITS) ?? Transform.Identity();
     render() {
         TraceMobx();
         return (
@@ -618,17 +617,17 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
                     {this.annotationLayer}
                     {this.overlayLayer}
                     {this._showWaiting ? <img alt="" className="pdfViewerDash-waiting" src="/assets/loading.gif" /> : null}
-                    {!this._mainCont.current || !this._annotationLayer.current ? null : (
+                    {!this._mainCont.current || !this._annotationLayer.current || !this.props.pdfBox.DocumentView ? null : (
                         <MarqueeAnnotator
                             ref={this._marqueeref}
                             Document={this._props.Document}
                             getPageFromScroll={this.getPageFromScroll}
                             anchorMenuClick={this._props.anchorMenuClick}
                             scrollTop={0}
-                            isNativeScaled
                             annotationLayerScrollTop={NumCast(this._props.Document._layout_scrollTop)}
                             addDocument={this.addDocumentWrapper}
-                            docView={this._props.pdfBox.DocumentView!}
+                            docView={this.props.pdfBox.DocumentView}
+                            screenTransform={this.screenToMarqueeXf}
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
                             selectionText={this.selectionText}
-- 
cgit v1.2.3-70-g09d2


From 3f6a168b2916ccac707cf5ea1e4ef898a470d7d1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 13 Feb 2025 23:27:42 -0500
Subject: lots of cleanup in GPTpopup.

---
 src/client/apis/gpt/GPT.ts                         |  20 +-
 .../views/collections/CollectionCardDeckView.tsx   |  20 +-
 src/client/views/global/globalScripts.ts           |   5 -
 src/client/views/nodes/DataVizBox/DataVizBox.tsx   |   4 +-
 src/client/views/nodes/WebBox.tsx                  |   4 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   6 +-
 src/client/views/pdf/AnchorMenu.tsx                |  17 +-
 src/client/views/pdf/GPTPopup/GPTPopup.scss        |  45 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 629 ++++++++-------------
 src/client/views/pdf/PDFViewer.tsx                 |   2 +-
 10 files changed, 285 insertions(+), 467 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index dc4607b94..066510a6a 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,6 +1,14 @@
 import { ChatCompletionMessageParam, Image } from 'openai/resources';
 import { openai } from './setup';
 
+export enum GPTTypeStyle {
+    AssignTags = 1,
+    Filter = 2,
+    DocInfo = 3,
+    ChooseDoc = 4,
+    GeneralInfo = 5,
+    SortDocs = 6,
+}
 enum GPTCallType {
     SUMMARY = 'summary',
     COMPLETION = 'completion',
@@ -127,12 +135,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
         temp: 0,
         prompt: `I'm going to provide you with a question. 
                  Based on the question, is the user asking you to 
-                     1. Assigns docs with tags(like star / heart etc)/labels, 
-                     2. Filter docs, 
-                     3. Provide information about a specific doc 
-                     4. Provide a specific doc based on a question/information 
-                     5. Provide general information 
-                     6. Put cards in a specific order. 
+                     ${GPTTypeStyle.AssignTags}. Assigns docs with tags(like star / heart etc)/labels, 
+                     ${GPTTypeStyle.ChooseDoc}. Filter docs, 
+                     ${GPTTypeStyle.DocInfo}. Provide information about a specific doc 
+                     ${GPTTypeStyle.Filter}. Provide a specific doc 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-6. For number one, provide the number (1) and the appropriate tag`,
     },
     subset: {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 43464e50c..a3ec3884b 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -12,7 +12,7 @@ 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 } from '../../apis/gpt/GPT';
+import { gptImageLabel, GPTTypeStyle } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
@@ -80,7 +80,7 @@ export class CollectionCardView extends CollectionSubView() {
     childPairStringListAndUpdateSortDesc = () =>
         this.childPairStringList().then(sortDesc => {
             GPTPopup.Instance.setSortDesc(sortDesc.join());
-            GPTPopup.Instance.onSortComplete = this.processGptOutput;
+            GPTPopup.Instance.onGptResponse = this.processGptResponse;
             GPTPopup.Instance.onQuizRandom = this.quizMode;
         });
 
@@ -113,7 +113,7 @@ export class CollectionCardView extends CollectionSubView() {
     onGptHide = () => Doc.setDocFilter(this.Document, 'tags', '#chat', 'remove');
     componentWillUnmount() {
         GPTPopup.Instance.setSortDesc('');
-        GPTPopup.Instance.onSortComplete = undefined;
+        GPTPopup.Instance.onGptResponse = undefined;
         GPTPopup.Instance.onQuizRandom = undefined;
         GPTPopup.Instance.setRegenerateCallback(undefined, null);
         Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
@@ -443,18 +443,18 @@ export class CollectionCardView extends CollectionSubView() {
      * @param questionType
      * @param tag
      */
-    processGptOutput = (gptOutput: string, questionType: string, tag?: string) =>
+    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 === '2' || questionType === '4') {
+            if (questionType === GPTTypeStyle.Filter || questionType === GPTTypeStyle.ChooseDoc) {
                 this.childDocs.forEach(d => {
                     TagItem.removeTagFromDoc(d, '#chat');
                 });
             }
 
-            if (questionType === '6') {
+            if (questionType === GPTTypeStyle.SortDocs) {
                 this.Document[this._props.fieldKey + '_sort'] = docSortings.Chat;
             }
 
@@ -464,18 +464,18 @@ export class CollectionCardView extends CollectionSubView() {
                 const doc = this._textToDoc.get(normalizedItem);
                 if (doc) {
                     switch (questionType) {
-                        case '6':
+                        case GPTTypeStyle.SortDocs:
                             doc.chatIndex = index;
                             break;
-                        case '1':
+                        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 '2':
-                        case '4':
+                        case GPTTypeStyle.Filter:
+                        case GPTTypeStyle.ChooseDoc:
                             TagItem.addTagToDoc(doc, '#chat');
                             Doc.setDocFilter(this.Document, 'tags', '#chat', 'check');
                             break;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index b44292164..029c4dbc7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -228,15 +228,10 @@ ScriptingGlobals.add(function showFreeform(
                 if (GPTPopup.Instance.Visible){
                     doc[Doc.LayoutFieldKey(doc)+"_sort"] = '';
                     GPTPopup.Instance.setVisible(false);
-   
                 } else {
                     GPTPopup.Instance.setVisible(true);
                     GPTPopup.Instance.setMode(GPTPopupMode.CARD);
-                    GPTPopup.Instance.setCardsDoneLoading(true);
-
                 }
-                
-
             },
         }],
         ['toggle-tags', {
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index b874d077b..fa3ab73a7 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -489,7 +489,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
         }
 
         // Changing which document to add the annotation to (the currently selected PDF)
-        GPTPopup.Instance.setSidebarId('data_sidebar');
+        GPTPopup.Instance.setSidebarFieldKey('data_sidebar');
         GPTPopup.Instance.addDoc = this.sidebarAddDocument;
     };
 
@@ -523,7 +523,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
     };
 
     askGPT = action(async () => {
-        GPTPopup.Instance.setSidebarId('data_sidebar');
+        GPTPopup.Instance.setSidebarFieldKey('data_sidebar');
         GPTPopup.Instance.addDoc = this.sidebarAddDocument;
         GPTPopup.Instance.createFilteredDoc = this.createFilteredDoc;
         GPTPopup.Instance.setDataJson('');
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 6026d9ca7..e7a10cc29 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -383,7 +383,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
                 this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined);
                 AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale);
                 // Changing which document to add the annotation to (the currently selected WebBox)
-                GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
+                GPTPopup.Instance.setSidebarFieldKey(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
                 GPTPopup.Instance.addDoc = this.sidebarAddDocument;
             }
         } else {
@@ -446,7 +446,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
             this._textAnnotationCreator = () => this.createTextAnnotation(sel, selRange);
             (!sel.isCollapsed || this.marqueeing) && AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
             // Changing which document to add the annotation to (the currently selected WebBox)
-            GPTPopup.Instance.setSidebarId(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
+            GPTPopup.Instance.setSidebarFieldKey(`${this._props.fieldKey}_${this._urlHash ? this._urlHash + '_' : ''}sidebar`);
             GPTPopup.Instance.addDoc = this.sidebarAddDocument;
         }
     };
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index eb1f9d07b..17b9bce47 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -135,7 +135,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     /**
      * ApplyingChange - Marks whether an interactive text edit is currently in the process of being written to the database.
-     * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to 
+     * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to
      * the prototype or other external edits
      */
     public ApplyingChange: string = '';
@@ -1043,7 +1043,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
 
     askGPT = action(async () => {
         try {
-            GPTPopup.Instance.setSidebarId(this.sidebarKey);
+            GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey);
             GPTPopup.Instance.addDoc = this.sidebarAddDocument;
             const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
             if (!res) {
@@ -1660,7 +1660,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
         }
     };
     onSelectEnd = () => {
-        GPTPopup.Instance.setSidebarId(this.sidebarKey);
+        GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey);
         GPTPopup.Instance.addDoc = this.sidebarAddDocument;
         document.removeEventListener('pointerup', this.onSelectEnd);
     };
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 18da01890..f7070c780 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,5 +1,5 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from '@dash/components';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -9,16 +9,15 @@ import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtil
 import { emptyFunction, unimplementedFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
-import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
 import { SettingsManager } from '../../util/SettingsManager';
 import { undoBatch } from '../../util/UndoManager';
 import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
 import { LinkPopup } from '../linking/LinkPopup';
+import { ComparisonBox } from '../nodes/ComparisonBox';
 import { DocumentView } from '../nodes/DocumentView';
 import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
 import './AnchorMenu.scss';
-import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
-import { ComparisonBox } from '../nodes/ComparisonBox';
+import { GPTPopup } from './GPTPopup/GPTPopup';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -98,15 +97,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      * Invokes the API with the selected text and stores it in the summarized text.
      * @param e pointer down event
      */
-    gptSummarize = () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
-        GPTPopup.Instance.setLoading(true);
-        gptAPICall(this._selectedText, GPTCallType.SUMMARY)
-            .then(res => GPTPopup.Instance.setText(res || 'Something went wrong.'))
-            .catch(err => console.error(err))
-            .finally(() => GPTPopup.Instance.setLoading(false));
-    };
+    gptSummarize = () => GPTPopup.Instance.generateSummary(this._selectedText);
 
     /*
      * Transfers the flashcard text generated by GPT on flashcards and creates a collection out them.
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 0247dc10c..9cf318dc0 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -5,7 +5,7 @@ $lightgrey: #ececec;
 $button: #5b97ff;
 $highlightedText: #82e0ff;
 
-.summary-box {
+.gptPopup-summary-box {
     position: fixed;
     top: 115px;
     left: 75px;
@@ -35,7 +35,7 @@ $highlightedText: #82e0ff;
         right: 0;
         bottom: 0;
         cursor: se-resize;
-      }
+    }
 
     .summary-heading {
         display: flex;
@@ -63,12 +63,12 @@ $highlightedText: #82e0ff;
         cursor: pointer;
     }
 
-    .content-wrapper {
+    .gptPopup-content-wrapper {
         padding-top: 10px;
         min-height: 50px;
         // max-height: 150px;
         overflow-y: auto;
-        height: 100%
+        height: 100%;
     }
 
     .btns-wrapper-gpt {
@@ -78,7 +78,7 @@ $highlightedText: #82e0ff;
         align-items: center;
         flex-direction: column;
 
-        .inputWrapper{
+        .inputWrapper {
             display: flex;
             justify-content: center;
             align-items: center;
@@ -87,17 +87,15 @@ $highlightedText: #82e0ff;
             bottom: 0;
             width: 100%;
             background-color: white;
-
-
         }
 
-        .searchBox-input{
+        .searchBox-input {
             height: 40px;
             border-radius: 10px;
             position: absolute;
             bottom: 10px;
             border-color: #5b97ff;
-            width: 90%
+            width: 90%;
         }
 
         .chat-wrapper {
@@ -106,52 +104,41 @@ $highlightedText: #82e0ff;
             width: 100%;
             max-height: calc(100vh - 80px);
             overflow-y: auto;
-            padding-bottom: 60px; 
+            padding-bottom: 60px;
         }
-    
+
         .chat-bubbles {
             margin-top: 20px;
             display: flex;
             flex-direction: column;
             flex-grow: 1;
         }
-    
+
         .chat-bubble {
             padding: 10px;
             margin-bottom: 10px;
             border-radius: 10px;
             max-width: 60%;
         }
-    
+
         .user-message {
             background-color: #283d53;
             align-self: flex-end;
             color: whitesmoke;
         }
-    
+
         .chat-message {
             background-color: #367ae7;
             align-self: flex-start;
-            color:whitesmoke;
+            color: whitesmoke;
         }
 
-
-        
-
         .summarizing {
             display: flex;
             align-items: center;
         }
-
-        
-
-        
-
-        
     }
 
-
-
     .text-btn {
         &:hover {
             background-color: $button;
@@ -198,18 +185,12 @@ $highlightedText: #82e0ff;
     color: #666;
 }
 
-
-
-
-
 @keyframes spin {
     to {
         transform: rotate(360deg);
     }
 }
 
-
-
 .image-content-wrapper {
     display: flex;
     flex-direction: column;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 2d95ac2eb..691c24792 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -1,6 +1,6 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Button, IconButton, Toggle, ToggleType, Type } from '@dash/components';
-import { action, makeObservable, observable, runInAction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { CgClose, CgCornerUpLeft } from 'react-icons/cg';
@@ -10,7 +10,7 @@ import { ClientUtils } from '../../../../ClientUtils';
 import { Doc } from '../../../../fields/Doc';
 import { NumCast, StrCast } from '../../../../fields/Types';
 import { Networking } from '../../../Network';
-import { GPTCallType, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
+import { GPTCallType, GPTTypeStyle, gptAPICall, gptImageCall } from '../../../apis/gpt/GPT';
 import { DocUtils } from '../../../documents/DocUtils';
 import { Docs } from '../../../documents/Documents';
 import { SettingsManager } from '../../../util/SettingsManager';
@@ -19,7 +19,6 @@ import { ObservableReactComponent } from '../../ObservableReactComponent';
 import { DocumentView } from '../../nodes/DocumentView';
 import { AnchorMenu } from '../AnchorMenu';
 import './GPTPopup.scss';
-import { isJSDocProtectedTag } from 'typescript';
 
 export enum GPTPopupMode {
     SUMMARY,
@@ -42,133 +41,79 @@ export enum GPTQuizType {
 export class GPTPopup extends ObservableReactComponent<object> {
     // eslint-disable-next-line no-use-before-define
     static Instance: GPTPopup;
-    private messagesEndRef: React.RefObject<HTMLDivElement>;
-
-    @observable private chatMode: boolean = false;
-    private correlatedColumns: string[] = [];
-
-    @observable public Visible: boolean = false;
-    @action public setVisible = (vis: boolean) => {
-        this.Visible = vis;
-    };
-    @observable public loading: boolean = false;
-    @action public setLoading = (loading: boolean) => {
-        this.loading = loading;
-    };
-    @observable public text: string = '';
-    @action public setText = (text: string) => {
-        this.text = text;
-    };
-    @observable public selectedText: string = '';
-    @action public setSelectedText = (text: string) => {
-        this.selectedText = text;
-    };
-    @observable public dataJson: string = '';
-    public dataChatPrompt: string | undefined = undefined;
-    @action public setDataJson = (text: string) => {
-        if (text === '') this.dataChatPrompt = '';
-        this.dataJson = text;
-    };
-
-    @observable public imgDesc: string = '';
-    @action public setImgDesc = (text: string) => {
-        this.imgDesc = text;
-    };
-
-    @observable public imgUrls: string[][] = [];
-    @action public setImgUrls = (imgs: string[][]) => {
-        this.imgUrls = imgs;
-    };
-
-    @observable public mode: GPTPopupMode = GPTPopupMode.SUMMARY;
-    @action public setMode = (mode: GPTPopupMode) => {
-        this.mode = mode;
-    };
-
-    @observable public highlightRange: number[] = [];
-    @action callSummaryApi = () => {};
-
-    @observable private done: boolean = false;
-    @action public setDone = (done: boolean) => {
-        this.done = done;
-        this.chatMode = false;
-    };
-
-    // change what can be a ref into a ref
-    @observable private sidebarId: string = '';
-    @action public setSidebarId = (id: string) => {
-        this.sidebarId = id;
-    };
-
-    @observable private imgTargetDoc: Doc | undefined;
-    @action public setImgTargetDoc = (anchor: Doc) => {
-        this.imgTargetDoc = anchor;
-    };
-
-    @observable private textAnchor: Doc | undefined;
-    @action public setTextAnchor = (anchor: Doc) => {
-        this.textAnchor = anchor;
+    private _regenerateCallback: (() => 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 _sortDesc: string = '';
+    private _sidebarFieldKey: string = '';
+    private _imgDesc: string = '';
+    private _selectedText: string = '';
+
+    public setImgDesc = (text: string) => (this._imgDesc = text);
+    public setSidebarFieldKey = (id: string) => (this._sidebarFieldKey = id);
+    public setSortDesc = (t: string) => (this._sortDesc = 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;
     };
 
-    @observable public sortDesc: string = '';
-    @action public setSortDesc = (t: string) => {
-        this.sortDesc = t;
-    };
-
-    onSortComplete?: (sortResult: string, questionType: string, tag?: string) => void;
-    onQuizRandom?: () => void;
-
-    @observable collectionDoc: Doc | undefined = undefined;
-    @action setCollectionDoc(doc: Doc | undefined) {
-        this.collectionDoc = doc;
-    }
-
-    @observable sortRespText: string = '';
-
-    @action setSortRespText(resp: string) {
-        this.sortRespText = resp;
+    constructor(props: object) {
+        super(props);
+        makeObservable(this);
+        GPTPopup.Instance = this;
+        this._messagesEndRef = React.createRef();
     }
 
-    @observable chatSortPrompt: string = '';
-
-    sortPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.chatSortPrompt = e.target.value;
-    });
+    componentDidUpdate = () => this._gptProcessing && this.setStopAnimatingResponse(false);
 
-    @observable quizAnswer: string = '';
-
-    quizAnswerChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.quizAnswer = e.target.value;
-    });
-
-    @observable conversationArray: string[] = ['Hi! In this pop up, you can ask ChatGPT questions about your documents and filter / sort them. '];
+    @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);
+    @observable private _responseText: string = '';
+    @action public 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);
+    @observable private _collectionContext: Doc | undefined = undefined;
+    @action setCollectionContext = (doc: Doc | undefined) => (this._collectionContext = doc);
+    @observable private _sortPrompt: string = '';
+    @action setSortPrompt = (e: React.ChangeEvent<HTMLInputElement>) => (this._sortPrompt = e.target.value);
+    @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);
 
     /**
-     * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
-     * @returns
+     * Callback function that causes the card view to update the childpair string list
+     * @param callback
      */
-    generateQuiz = (selected: Doc) =>
-        (this.regenerateCallback?.() ?? Promise.resolve()).then(() =>
-            this.generateRubric(selected).then(() =>
-                gptAPICall('Question: ' + StrCast(selected.gptInputText) + ' UserAnswer: ' + this.quizAnswer + '.  Rubric: ' + StrCast(selected.gptRubric), GPTCallType.QUIZ)
-                    .then(res => {
-                        if (res) {
-                            this.setQuizResp(res);
-                            this.conversationArray.push(res);
-                            this.onQuizRandom?.();
-                        } else {
-                            console.error('GPT provided no response');
-                        }
-                    })
-                    .catch(err => console.error('GPT call failed', err))
-            )
-        );
+    public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
+        this.setCollectionContext(collectionDoc);
+        this._regenerateCallback = callback;
+    }
+
+    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];
 
     /**
-     * Generates a rubric by which to compare the user's answer to
-     * @param inputText user's answer
+     * Generates a rubric for evaluating the user's description of the document's text
      * @param doc the doc the user is providing info about
-     * @returns gpt's response
+     * @returns gpt's response rubric
      */
     generateRubric = (doc: Doc) =>
         StrCast(doc.gptRubric)
@@ -177,148 +122,129 @@ export class GPTPopup extends ObservableReactComponent<object> {
                   .then(res => (doc.gptRubric = res))
                   .catch(err => console.error('GPT call failed', err));
 
-    @observable private regenerateCallback: (() => Promise<void>) | null = null;
-
     /**
-     * Callback function that causes the card view to update the childpair string list
-     * @param callback
+     * When the cards are in quiz mode in the card view, allows gpt to determine whether the user's answer was correct
+     * @param doc the doc the user is providing info about
+     * @param quizAnswer the user's answer/description for the document
+     * @returns
      */
-    @action public setRegenerateCallback(collectionDoc: Doc | undefined, callback: null | (() => Promise<void>)) {
-        this.setCollectionDoc(collectionDoc);
-        this.regenerateCallback = callback;
-    }
-
-    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;
-
-    @observable quizRespText: string = '';
-
-    @action setQuizResp(resp: string) {
-        this.quizRespText = resp;
-    }
+    generateQuizAnswerAnalysis = (doc: Doc, quizAnswer: string) =>
+        (this._regenerateCallback?.() ?? Promise.resolve()).then(
+            () =>
+            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
+        );
 
     /**
      * Generates a response to the user's question depending on the type of their question
+     * @param userPrompt the user's input that chat will respond to
      */
-    generateCard = async () => {
-        this.setLoading(true);
-
-        await this.regenerateCallback?.();
-
-        try {
-            const questionType = await gptAPICall(this.chatSortPrompt, GPTCallType.TYPE);
-            const questionNumber = questionType.split(' ')[0][0];
-            const res = await (() => {
-                switch (questionNumber) {
-                    case '1':
-                    case '2':
-                    case '4': return gptAPICall(this.sortDesc, GPTCallType.SUBSET, this.chatSortPrompt);
-                    case '6': return gptAPICall(this.sortDesc, GPTCallType.SORT, this.chatSortPrompt);
-                    default: return gptAPICall(StrCast(DocumentView.SelectedDocs().lastElement()?.gptInputText), GPTCallType.INFO, this.chatSortPrompt);
-                }})(); // prettier-ignore
-
-            // Trigger the callback with the result
-            if (this.onSortComplete) {
-                this.onSortComplete(res || 'Something went wrong :(', questionNumber, questionType.split(' ').slice(1).join(' '));
-
-                let explanation = res;
-
-                if (questionType != '5' && questionType != '3') {
-                    // Extract explanation surrounded by ------ at the top or both at the top and bottom
-                    const explanationMatch = res.match(/------\s*([\s\S]*?)\s*(?:------|$)/) || [];
-                    explanation = explanationMatch[1] ? explanationMatch[1].trim() : 'No explanation found';
-                }
-
-                // Set the extracted explanation to sortRespText
-                this.setSortRespText(explanation);
-                runInAction(() => this.conversationArray.push(this.sortRespText));
-                this.scrollToBottom();
-
-                console.log(res);
-            }
-        } catch (err) {
-            console.error(err);
-        }
-
-        this.setLoading(false);
-    };
+    generateQueryResponse = (userPrompt: string) =>
+        (this._regenerateCallback ?? Promise.resolve)().then(() =>
+            gptAPICall(userPrompt, GPTCallType.TYPE).then(questionType =>
+                (() => {
+                    switch (this.questionTypeNumberToStyle(questionType)) {
+                        case GPTTypeStyle.AssignTags:
+                        case GPTTypeStyle.Filter:
+                        case GPTTypeStyle.ChooseDoc: return gptAPICall(this._sortDesc, GPTCallType.SUBSET, userPrompt);
+                        case GPTTypeStyle.SortDocs:  return gptAPICall(this._sortDesc, 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))
+            ).catch(err => console.log(err))
+        ); // prettier-ignore
 
     /**
      * Generates a Dalle image and uploads it to the server.
      */
-    generateImage = async () => {
-        if (this.imgDesc === '') return undefined;
-        this.setImgUrls([]);
-        this.setMode(GPTPopupMode.IMAGE);
-        this.setVisible(true);
-        this.setLoading(true);
-
-        try {
-            const imageUrls = await gptImageCall(this.imgDesc);
-            if (imageUrls && imageUrls[0]) {
-                const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] });
-                const source = ClientUtils.prepend(result.accessPaths.agnostic.client);
-                this.setImgUrls([[imageUrls[0], source]]);
-            }
-        } catch (err) {
-            console.error(err);
+    generateImage = () => {
+        if (this._imgDesc !== '') {
+            this.setImgUrls([]);
+            this.setMode(GPTPopupMode.IMAGE);
+            this.setVisible(true);
+            this.setGptProcessing(true);
+
+            return gptImageCall(this._imgDesc)
+                .then(imageUrls =>
+                    imageUrls?.[0]
+                        ? Networking.PostToServer('/uploadRemoteImage', { sources: [imageUrls[0]] }).then(res => {
+                              const source = ClientUtils.prepend(res[0].accessPaths.agnostic.client);
+                              return this.setImgUrls([[imageUrls[0]!, source]]);
+                          })
+                        : undefined
+                )
+                .catch(err => console.error(err))
+                .finally(() => this.setGptProcessing(false));
         }
-        this.setLoading(false);
         return undefined;
     };
 
     /**
-     * Completes an API call to generate a summary of
-     * this.selectedText in the popup.
+     * Completes an API call to generate a summary of the specified text
+     *
+     * @param text the text to summarizz
      */
-    generateSummary = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
-        GPTPopup.Instance.setLoading(true);
-
-        try {
-            const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
-            GPTPopup.Instance.setText(res || 'Something went wrong.');
-        } catch (err) {
-            console.error(err);
-        }
-        GPTPopup.Instance.setLoading(false);
+    generateSummary = (text?: string) => {
+        this._selectedText = text ?? this._selectedText;
+        this.setVisible(true);
+        this.setMode(GPTPopupMode.SUMMARY);
+        this.setGptProcessing(true);
+        return gptAPICall(this._selectedText, GPTCallType.SUMMARY)
+            .then(res => this.setResponseText(res || 'Something went wrong.'))
+            .catch(err => console.error(err))
+            .finally(() => this.setGptProcessing(false));
     };
 
     /**
      * Completes an API call to generate an analysis of
      * this.dataJson in the popup.
      */
-    generateDataAnalysis = async () => {
-        GPTPopup.Instance.setVisible(true);
-        GPTPopup.Instance.setLoading(true);
-        try {
-            const res = await gptAPICall(this.dataJson, GPTCallType.DATA, this.dataChatPrompt);
-            const json = JSON.parse(res! as string);
-            const keys = Object.keys(json);
-            this.correlatedColumns = [];
-            this.correlatedColumns.push(json[keys[0]]);
-            this.correlatedColumns.push(json[keys[1]]);
-            GPTPopup.Instance.setText(json[keys[2]] || 'Something went wrong.');
-        } catch (err) {
-            console.error(err);
-        }
-        GPTPopup.Instance.setLoading(false);
+    generateDataAnalysis = () => {
+        this.setVisible(true);
+        this.setGptProcessing(true);
+        return gptAPICall(this._dataJson, GPTCallType.DATA, this._dataChatPrompt)
+            .then(res => {
+                const json = JSON.parse(res! as string);
+                const keys = Object.keys(json);
+                this._correlatedColumns = [];
+                this._correlatedColumns.push(json[keys[0]]);
+                this._correlatedColumns.push(json[keys[1]]);
+                this.setResponseText(json[keys[2]] || 'Something went wrong.');
+            })
+            .catch(err => console.error(err))
+            .finally(() => this.setGptProcessing(false));
     };
 
     /**
      * Transfers the summarization text to a sidebar annotation text document.
      */
     private transferToText = () => {
-        const newDoc = Docs.Create.TextDocument(this.text.trim(), {
+        const newDoc = Docs.Create.TextDocument(this._responseText.trim(), {
             _width: 200,
             _height: 50,
             _layout_fitWidth: true,
             _layout_autoHeight: true,
         });
-        this.addDoc(newDoc, this.sidebarId);
-        // newDoc.data = 'Hello world';
+        this.addDoc(newDoc, this._sidebarFieldKey);
         const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
         if (anchor) {
             DocUtils.MakeLink(newDoc, anchor, {
@@ -330,73 +256,35 @@ export class GPTPopup extends ObservableReactComponent<object> {
     /**
      * Creates a histogram to show the correlation relationship that was found
      */
-    private createVisualization = () => {
-        this.createFilteredDoc(this.correlatedColumns);
-    };
+    private createVisualization = () => this.createFilteredDoc(this._correlatedColumns);
 
     /**
      * Transfers the image urls to actual image docs
      */
     private transferToImage = (source: string) => {
-        const textAnchor = this.textAnchor ?? this.imgTargetDoc;
-        if (!textAnchor) return;
-        const newDoc = Docs.Create.ImageDocument(source, {
-            x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
-            y: NumCast(textAnchor.y),
-            _height: 200,
-            _width: 200,
-            data_nativeWidth: 1024,
-            data_nativeHeight: 1024,
-        });
-        if (Doc.IsInMyOverlay(textAnchor)) {
-            newDoc.overlayX = textAnchor.x;
-            newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
-            Doc.AddToMyOverlay(newDoc);
-        } else {
-            this.addToCollection?.(newDoc);
-        }
-        // Create link between prompt and image
-        DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
-    };
-
-    /**
-     * Creates a chatbox for analyzing data so that users can ask specific questions.
-     */
-    private chatWithAI = () => {
-        this.chatMode = true;
-    };
-    dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
-        this.dataChatPrompt = e.target.value;
-    });
-
-    private getPreviewUrl = (source: string) => source.split('.').join('_m.');
-
-    constructor(props: object) {
-        super(props);
-        makeObservable(this);
-        GPTPopup.Instance = this;
-        this.messagesEndRef = React.createRef();
-    }
-
-    scrollToBottom = () => {
-        setTimeout(() => {
-            // Code to execute after 1 second (1000 ms)
-            if (this.messagesEndRef.current) {
-                this.messagesEndRef.current.scrollIntoView({ behavior: 'smooth', block: 'end' });
+        const textAnchor = this._textAnchor ?? this._imgTargetDoc;
+        if (textAnchor) {
+            const newDoc = Docs.Create.ImageDocument(source, {
+                x: NumCast(textAnchor.x) + NumCast(textAnchor._width) + 10,
+                y: NumCast(textAnchor.y),
+                _height: 200,
+                _width: 200,
+                data_nativeWidth: 1024,
+                data_nativeHeight: 1024,
+            });
+            if (Doc.IsInMyOverlay(textAnchor)) {
+                newDoc.overlayX = textAnchor.x;
+                newDoc.overlayY = NumCast(textAnchor.y) + NumCast(textAnchor._height);
+                Doc.AddToMyOverlay(newDoc);
+            } else {
+                this.addToCollection?.(newDoc);
             }
-        }, 50);
-    };
-
-    componentDidUpdate = () => {
-        if (this.loading) {
-            this.setDone(false);
+            // Create link between prompt and image
+            DocUtils.MakeLink(textAnchor, newDoc, { link_relationship: 'Image Prompt' });
         }
     };
 
-    @observable quizMode: GPTQuizType = GPTQuizType.CURRENT;
-    @action setQuizMode(g: GPTQuizType) {
-        this.quizMode = g;
-    }
+    scrollToBottom = () => setTimeout(() => this._messagesEndRef.current?.scrollIntoView({ behavior: 'smooth', block: 'end' }), 50);
 
     cardMenu = () => (
         <div className="btns-wrapper-gpt">
@@ -419,7 +307,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                 tooltip="Test your knowledge with ChatGPT!"
                 text="Quiz Cards!"
                 onClick={() => {
-                    this.conversationArray = ['Define the selected card!'];
+                    this._conversationArray = ['Define the selected card!'];
                     this.setMode(GPTPopupMode.QUIZ);
                     this.onQuizRandom?.();
                 }}
@@ -437,27 +325,19 @@ export class GPTPopup extends ObservableReactComponent<object> {
     );
 
     @action
-    handleKeyPress = (e: React.KeyboardEvent, isSort: boolean) => {
+    handleKeyPress = async (e: React.KeyboardEvent, isSort: boolean) => {
         if (e.key === 'Enter') {
             e.stopPropagation();
 
+            this.setGptProcessing(true);
             if (isSort) {
-                this.conversationArray.push(this.chatSortPrompt);
-                this.generateCard().then(
-                    action(() => {
-                        this.chatSortPrompt = '';
-                    })
-                );
+                this._conversationArray.push(this._sortPrompt);
+                await this.generateQueryResponse(this._sortPrompt).then(action(() => (this._sortPrompt = '')));
             } else {
-                this.conversationArray.push(this.quizAnswer);
-                this.setLoading(true);
-                this.generateQuiz(DocumentView.SelectedDocs().lastElement()).then(
-                    action(() => {
-                        this.quizAnswer = '';
-                        this.setLoading(false);
-                    })
-                );
+                this._conversationArray.push(this._quizAnswer);
+                await this.generateQuizAnswerAnalysis(DocumentView.SelectedDocs().lastElement(), this._quizAnswer).then(action(() => (this._quizAnswer = '')));
             }
+            this.setGptProcessing(false);
 
             this.scrollToBottom();
         }
@@ -467,26 +347,24 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <div className="btns-wrapper-gpt">
             <div className="chat-wrapper">
                 <div className="chat-bubbles">
-                    {this.conversationArray.map((message, index) => (
+                    {this._conversationArray.map((message, index) => (
                         <div key={index} className={`chat-bubble ${index % 2 === 1 ? 'user-message' : 'chat-message'}`}>
                             {message}
                         </div>
                     ))}
-                    {this.loading && <div className={`chat-bubble chat-message`}>...</div>}
+                    {this._gptProcessing && <div className={`chat-bubble chat-message`}>...</div>}
                 </div>
 
-                <div ref={this.messagesEndRef} style={{ height: '100px' }} />
+                <div ref={this._messagesEndRef} style={{ height: '100px' }} />
             </div>
             <div className="inputWrapper">
                 <input
                     className="searchBox-input"
                     defaultValue=""
-                    value={isSort ? this.chatSortPrompt : this.quizAnswer} // Controlled input
+                    value={isSort ? this._sortPrompt : this._quizAnswer} // Controlled input
                     autoComplete="off"
-                    onChange={isSort ? this.sortPromptChanged : this.quizAnswerChanged}
-                    onKeyDown={e => {
-                        this.handleKeyPress(e, isSort);
-                    }}
+                    onChange={isSort ? this.setSortPrompt : this.setQuizAnswer}
+                    onKeyDown={e => this.handleKeyPress(e, isSort)}
                     type="text"
                     placeholder={`${isSort ? 'Have ChatGPT sort, tag, define, or filter your cards for you!' : 'Define the selected card!'}`}
                 />
@@ -497,7 +375,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
     sortBox = (isSort: boolean) => (
         <div className="gptPopup-sortBox" style={{ height: '80%' }}>
             {this.heading(isSort ? 'SORTING' : 'QUIZ')}
-            {this.mode === GPTPopupMode.CARD ? this.cardMenu() : this.cardActual(isSort)}
+            {this._mode === GPTPopupMode.CARD ? this.cardMenu() : this.cardActual(isSort)}
         </div>
     );
 
@@ -505,7 +383,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
             {this.heading('GENERATED IMAGE')}
             <div className="image-content-wrapper">
-                {this.imgUrls.map((rawSrc, i) => (
+                {this._imgUrls.map((rawSrc, i) => (
                     <div key={rawSrc[0] + i} className="img-wrapper">
                         <div className="img-container">
                             <img key={rawSrc[0]} src={rawSrc[0]} width={150} height={150} alt="dalle generation" />
@@ -516,7 +394,7 @@ export class GPTPopup extends ObservableReactComponent<object> {
                     </div>
                 ))}
             </div>
-            {!this.loading && <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
+            {this._gptProcessing ? null : <IconButton tooltip="Generate Again" onClick={this.generateImage} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />}
         </div>
     );
 
@@ -524,44 +402,35 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <>
             <div>
                 {this.heading('SUMMARY')}
-                <div className="content-wrapper">
-                    {!this.loading &&
-                        (!this.done ? (
+                <div className="gptPopup-content-wrapper">
+                    {!this._gptProcessing &&
+                        (!this._stopAnimatingResponse ? (
                             <TypeAnimation
                                 speed={50}
                                 sequence={[
-                                    this.text,
+                                    this._responseText,
                                     () => {
-                                        setTimeout(() => {
-                                            this.setDone(true);
-                                        }, 500);
+                                        setTimeout(() => this.setStopAnimatingResponse(true), 500);
                                     },
                                 ]}
                             />
                         ) : (
-                            this.text
+                            this._responseText
                         ))}
                 </div>
             </div>
-            {!this.loading && (
+            {!this._gptProcessing && (
                 <div className="btns-wrapper">
-                    {this.done ? (
+                    {this._stopAnimatingResponse ? (
                         <>
-                            <IconButton tooltip="Generate Again" onClick={this.generateSummary /* this.callSummaryApi */} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
+                            <IconButton tooltip="Generate Again" onClick={() => this.generateSummary(this._selectedText + ' ')} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(SettingsManager.userVariantColor)} />
                             <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
                         </>
                     ) : (
                         <div className="summarizing">
                             <span>Summarizing</span>
                             <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
-                            <Button
-                                text="Stop Animation"
-                                onClick={() => {
-                                    this.setDone(true);
-                                }}
-                                color={StrCast(SettingsManager.userVariantColor)}
-                                type={Type.TERT}
-                            />
+                            <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SettingsManager.userVariantColor)} type={Type.TERT} />
                         </div>
                     )}
                 </div>
@@ -573,33 +442,31 @@ export class GPTPopup extends ObservableReactComponent<object> {
         <>
             <div>
                 {this.heading('ANALYSIS')}
-                <div className="content-wrapper">
-                    {!this.loading &&
-                        (!this.done ? (
+                <div className="gptPopup-content-wrapper">
+                    {!this._gptProcessing &&
+                        (!this._stopAnimatingResponse ? (
                             <TypeAnimation
                                 speed={50}
                                 sequence={[
-                                    this.text,
+                                    this._responseText,
                                     () => {
-                                        setTimeout(() => {
-                                            this.setDone(true);
-                                        }, 500);
+                                        setTimeout(() => this.setStopAnimatingResponse(true), 500);
                                     },
                                 ]}
                             />
                         ) : (
-                            this.text
+                            this._responseText
                         ))}
                 </div>
             </div>
-            {!this.loading && (
+            {!this._gptProcessing && (
                 <div className="btns-wrapper">
-                    {this.done ? (
-                        this.chatMode ? (
+                    {this._stopAnimatingResponse ? (
+                        this._chatEnabled ? (
                             <input
                                 defaultValue=""
                                 autoComplete="off"
-                                onChange={this.dataPromptChanged}
+                                onChange={e => (this._dataChatPrompt = e.target.value)}
                                 onKeyDown={e => {
                                     e.key === 'Enter' ? this.generateDataAnalysis() : null;
                                     e.stopPropagation();
@@ -613,21 +480,14 @@ export class GPTPopup extends ObservableReactComponent<object> {
                         ) : (
                             <>
                                 <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
-                                <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
+                                <Button tooltip="Chat with AI" text="Chat with AI" onClick={() => this.setChatEnabled(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
                             </>
                         )
                     ) : (
                         <div className="summarizing">
                             <span>Summarizing</span>
                             <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
-                            <Button
-                                text="Stop Animation"
-                                onClick={() => {
-                                    this.setDone(true);
-                                }}
-                                color={StrCast(SnappingManager.userVariantColor)}
-                                type={Type.TERT}
-                            />
+                            <Button text="Stop Animation" onClick={() => this.setStopAnimatingResponse(true)} color={StrCast(SnappingManager.userVariantColor)} type={Type.TERT} />
                         </div>
                     )}
                 </div>
@@ -636,70 +496,53 @@ export class GPTPopup extends ObservableReactComponent<object> {
     );
 
     aiWarning = () =>
-        this.done ? (
+        !this._stopAnimatingResponse ? null : (
             <div className="ai-warning">
                 <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
                 AI generated responses can contain inaccurate or misleading content.
             </div>
-        ) : null;
+        );
 
     heading = (headingText: string) => (
         <div className="summary-heading">
             <label className="summary-text">{headingText}</label>
-            {this.loading ? (
+            {this._gptProcessing ? (
                 <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />
             ) : (
                 <>
-                    {(this.mode === GPTPopupMode.SORT || this.mode === GPTPopupMode.QUIZ) && (
-                        <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this.mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
+                    {(this._mode === GPTPopupMode.SORT || this._mode === GPTPopupMode.QUIZ) && (
+                        <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="back" icon={<CgCornerUpLeft size="16px" />} onClick={() => (this._mode = GPTPopupMode.CARD)} style={{ right: '50px', position: 'absolute' }} />
                     )}
                     <Toggle
                         tooltip="Clear Chat filter"
                         toggleType={ToggleType.BUTTON}
                         type={Type.PRIM}
-                        toggleStatus={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat')}
-                        text={Doc.hasDocFilter(this.collectionDoc, 'tags', '#chat') ? 'filtered' : ''}
+                        toggleStatus={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat')}
+                        text={Doc.hasDocFilter(this._collectionContext, 'tags', '#chat') ? 'filtered' : ''}
                         color="red"
-                        onClick={() => this.collectionDoc && Doc.setDocFilter(this.collectionDoc, 'tags', '#chat', 'remove')}
-                    />
-                    <IconButton
-                        color={StrCast(SettingsManager.userVariantColor)}
-                        tooltip="close"
-                        icon={<CgClose size="16px" />}
-                        onClick={() => {
-                            this.setVisible(false);
-                        }}
+                        onClick={() => this._collectionContext && Doc.setDocFilter(this._collectionContext, 'tags', '#chat', 'remove')}
                     />
+                    <IconButton color={StrCast(SettingsManager.userVariantColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={() => this.setVisible(false)} />
                 </>
             )}
         </div>
     );
 
     render() {
-        let content;
-
-        switch (this.mode) {
-            case GPTPopupMode.SUMMARY:
-                content = this.summaryBox();
-                break;
-            case GPTPopupMode.DATA:
-                content = this.dataAnalysisBox();
-                break;
-            case GPTPopupMode.IMAGE:
-                content = this.imageBox();
-                break;
-            case GPTPopupMode.SORT:
-            case GPTPopupMode.CARD:
-            case GPTPopupMode.QUIZ:
-                content = this.sortBox(this.mode === GPTPopupMode.SORT);
-                break;
-            default:
-                content = null;
-        }
-
         return (
-            <div className="summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}>
-                {content}
+            <div className="gptPopup-summary-box" style={{ display: this.Visible ? 'flex' : 'none' }}>
+                {(() => {
+                    //prettier-ignore
+                    switch (this._mode) {
+                        case GPTPopupMode.SORT:
+                        case GPTPopupMode.CARD:
+                        case GPTPopupMode.QUIZ:    return this.sortBox(this._mode === GPTPopupMode.SORT);
+                        case GPTPopupMode.SUMMARY: return this.summaryBox();
+                        case GPTPopupMode.DATA:    return this.dataAnalysisBox();
+                        case GPTPopupMode.IMAGE:   return this.imageBox();
+                        default:                   return null;
+                }
+                })()}
                 <div className="resize-handle" />
             </div>
         );
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 8728ce99c..167421a4a 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -437,7 +437,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
             AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
         }
 
-        GPTPopup.Instance.setSidebarId('data_sidebar');
+        GPTPopup.Instance.setSidebarFieldKey('data_sidebar');
         GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
         // allows for creating collection
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
-- 
cgit v1.2.3-70-g09d2

