From e95d25eb8159bb7c753fa27e74e9baa8d3bffea6 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Sat, 30 Mar 2024 00:06:27 -0400
Subject: working on making flashcards from pdf with ai

---
 src/client/views/collections/CollectionView.tsx | 9 +++++++++
 1 file changed, 9 insertions(+)

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

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 18eb4dd1f..168176edf 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -176,6 +176,15 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                     return newRendition;
                 });
 
+            const revealOptions = cm.findByDescription('Filter Flashcards');
+            const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
+            revealItems.push({ description: 'All', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'all'), icon: 'eye-slash' });
+            revealItems.push({ description: 'Star', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'star'), icon: 'hand-point-up' });
+            revealItems.push({ description: 'Practice Mode', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'practice'), icon: 'rotate' });
+
+            //revealItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
+            !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+
             const options = cm.findByDescription('Options...');
             const optionItems = options && 'subitems' in options ? options.subitems : [];
             !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.Document.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.Document.forceActive = !this.Document.forceActive), icon: 'project-diagram' }) : null;
-- 
cgit v1.2.3-70-g09d2


From e0641c3175bc7cf53dea924524e51f1eefa6a8b1 Mon Sep 17 00:00:00 2001
From: aidahosa1 <aisosa_idahosa@brown.edu>
Date: Mon, 18 Mar 2024 13:39:34 -0400
Subject: the lack of pushing is astounding actually

---
 src/client/documents/DocumentTypes.ts              |   4 +-
 src/client/documents/Documents.ts                  |  47 ++++++
 .../views/collections/CollectionCardDeckView.tsx   | 164 +++++++++++++++++++++
 src/client/views/collections/CollectionView.tsx    |   5 +
 .../CollectionFreeFormLayoutEngines.tsx            |  34 +++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   4 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  20 ++-
 7 files changed, 274 insertions(+), 4 deletions(-)
 create mode 100644 src/client/views/collections/CollectionCardDeckView.tsx

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 1123bcac9..209b34f91 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -62,5 +62,7 @@ export enum CollectionViewType {
     Pile = 'pileup',
     StackedTimeline = 'stacked timeline',
     NoteTaking = 'notetaking',
-    Calendar = 'calendar'
+    Calendar = 'calendar',
+    Card = 'card'
+
 }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 17cb6fef8..d12c61fd7 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1219,6 +1219,15 @@ export namespace Docs {
             );
         }
 
+        export function CardDeckDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+            return InstanceFromProto(
+                Prototypes.get(DocumentType.COL),
+                new List(documents),
+                { backgroundColor: 'transparent', dropAction: dropActionType.move, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Card },
+                id
+            );
+        }
+
         export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id);
         }
@@ -1876,6 +1885,44 @@ export namespace DocUtils {
             return newCollection;
         }
     }
+
+    export function spreadCards(docList: Doc[], x: number = 0, y: number = 0, spreadAngle: number = 30, radius: number = 100, create: boolean = true) {
+        const totalCards = docList.length;
+        const halfSpreadAngle = spreadAngle * 0.5;
+        const angleStep = spreadAngle / (totalCards - 1);
+        
+        runInAction(() => {
+            docList.forEach((d, i) => {
+                DocUtils.iconify(d);
+                const angle = (-halfSpreadAngle + angleStep * i) * (Math.PI / 180); // Convert degrees to radians
+                d.x = x + Math.cos(angle) * radius;
+                d.y = y + Math.sin(angle) * radius;
+                d.rotation = angle; // Assuming 'd.rotation' sets the rotation, adjust accordingly
+                d._timecodeToShow = undefined;
+            });
+        });
+
+        if (create) {
+            const newCollection = Docs.Create.CardDeckDocument(docList, { 
+                title: 'card-spread', 
+                _freeform_noZoom: true, 
+                x: x - radius, 
+                y: y - radius, 
+                _width: radius * 2, 
+                _height: radius * 2, 
+                dragWhenActive: true, 
+                _layout_fitWidth: false 
+            });
+            // Adjust position based on the collection's dimensions if needed
+            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - radius;
+            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - radius;
+            newCollection._width = newCollection._height = radius * 2;
+            return newCollection;
+        }
+    }
+
+
+
     export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) {
         const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from');
         if (!portalLink) {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
new file mode 100644
index 000000000..feb9e61cc
--- /dev/null
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -0,0 +1,164 @@
+import { action, computed, IReactionDisposer, makeObservable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { ScriptField } from '../../../fields/ScriptField';
+import { NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { DocUtils } from '../../documents/Documents';
+import { SelectionManager } from '../../util/SelectionManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
+import { computePassLayout, computeStarburstLayout, computeCardDeckLayout } from './collectionFreeForm';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import './CollectionPileView.scss';
+import { CollectionSubView } from './CollectionSubView';
+import { dropActionType } from '../../util/DragManager';
+
+@observer
+export class CollectionCardView extends CollectionSubView() {
+    _originalChrome: any = '';
+    _disposers: { [name: string]: IReactionDisposer } = {};
+
+    constructor(props: any) {
+        super(props);
+        makeObservable(this);
+    }
+
+    componentDidMount() {
+        if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeCardDeckLayout.name) {
+            this.Document._freeform_cardEngine = computePassLayout.name;
+        }
+        this._originalChrome = this.layoutDoc._chromeHidden;
+        this.layoutDoc._chromeHidden = true;
+    }
+    componentWillUnmount() {
+        this.layoutDoc._chromeHidden = this._originalChrome; 
+        Object.values(this._disposers).forEach(disposer => disposer?.());
+    }
+
+    layoutEngine = () => StrCast(this.Document._freeform_cardEngine);
+
+    @undoBatch
+    addCardDoc = (doc: Doc | Doc[]) => {
+        (doc instanceof Doc ? [doc] : doc).map(d => DocUtils.iconify(d));
+        return this._props.addDocument?.(doc) || false;
+    };
+
+    @undoBatch
+    removeCardDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
+        (doc instanceof Doc ? [doc] : doc).forEach(d => Doc.deiconifyView(d));
+        const ret = this._props.moveDocument?.(doc, targetCollection, addDoc) || false;
+        if (ret && !DocListCast(this.dataDoc[this.fieldKey ?? 'data']).length) this.DocumentView?.()._props.removeDocument?.(this.Document);
+        return ret;
+    };
+
+    @computed get toggleIcon() {
+        return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' });
+    }
+    @computed get contentEvents() {
+        const isCard = this.layoutEngine() === computeCardDeckLayout.name;
+        return this._props.isContentActive() && isCard ? undefined : 'none';
+    }
+
+    // returns the contents of the cardSpread in a CollectionFreeFormView
+    @computed get contents() {
+        return (
+            <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}>
+                <CollectionFreeFormView
+                    {...this._props} //
+                    layoutEngine={this.layoutEngine}
+                    addDocument={this.addCardDoc}
+                    moveDocument={this.removeCardDoc}
+                    // pile children never have their contents active, but will be document active whenever the entire pile is.
+                    childContentsActive={returnFalse}
+                    childDocumentsActive={this._props.isDocumentActive}
+                    childDragAction={dropActionType.move}
+                    childClickScript={this.toggleIcon}
+                />
+            </div>
+        );
+    }
+
+    // // toggles the pileup between starburst to compact
+    // toggleStarburst = action(() => {
+    //     this.layoutDoc._freeform_scale = undefined;
+    //     if (this.layoutEngine() === computeStarburstLayout.name) {
+    //         if (NumCast(this.layoutDoc._width) !== NumCast(this.Document._starburstDiameter, 500)) {
+    //             this.Document._starburstDiameter = NumCast(this.layoutDoc._width);
+    //         }
+    //         const defaultSize = 110;
+    //         this.Document.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width) / 2 - NumCast(this.layoutDoc._freeform_pileWidth, defaultSize) / 2;
+    //         this.Document.y = NumCast(this.Document.y) + NumCast(this.layoutDoc._height) / 2 - NumCast(this.layoutDoc._freeform_pileHeight, defaultSize) / 2;
+    //         this.layoutDoc._width = NumCast(this.layoutDoc._freeform_pileWidth, defaultSize);
+    //         this.layoutDoc._height = NumCast(this.layoutDoc._freeform_pileHeight, defaultSize);
+    //         DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
+    //         this.layoutDoc._freeform_panX = 0;
+    //         this.layoutDoc._freeform_panY = -10;
+    //         this.Document._freeform_pileEngine = computePassLayout.name;
+    //     } else {
+    //         const defaultSize = NumCast(this.Document._starburstDiameter, 400);
+    //         this.Document.x = NumCast(this.Document.x) + NumCast(this.layoutDoc._width) / 2 - defaultSize / 2;
+    //         this.Document.y = NumCast(this.Document.y) + NumCast(this.layoutDoc._height) / 2 - defaultSize / 2;
+    //         this.layoutDoc._freeform_pileWidth = NumCast(this.layoutDoc._width);
+    //         this.layoutDoc._freeform_pileHeight = NumCast(this.layoutDoc._height);
+    //         this.layoutDoc._freeform_panX = this.layoutDoc._freeform_panY = 0;
+    //         this.layoutDoc._width = this.layoutDoc._height = defaultSize;
+    //         this.layoutDoc.background;
+    //         this.Document._freeform_pileEngine = computeStarburstLayout.name;
+    //     }
+    // });
+
+    // for dragging documents out of the pileup view
+    _undoBatch: UndoManager.Batch | undefined;
+    pointerDown = (e: React.PointerEvent) => {
+        let dist = 0;
+        setupMoveUpEvents(
+            this,
+            e,
+            (e: PointerEvent, down: number[], delta: number[]) => {
+                if (this.layoutEngine() === 'pass' && this.childDocs.length && e.shiftKey) {
+                    dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
+                    if (dist > 100) {
+                        if (!this._undoBatch) {
+                            this._undoBatch = UndoManager.StartBatch('layout pile');
+                        }
+                        const doc = this.childDocs[0];
+                        doc.x = e.clientX;
+                        doc.y = e.clientY;
+                        this._props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this._props.removeDocument?.(doc) || false);
+                        dist = 0;
+                    }
+                }
+                return false;
+            },
+            () => {
+                this._undoBatch?.end();
+                this._undoBatch = undefined;
+            },
+            emptyFunction,
+            e.shiftKey && this.layoutEngine() === computePassLayout.name,
+            this.layoutEngine() === computePassLayout.name && e.shiftKey
+        ); // this sets _doubleTap
+    };
+
+    // onClick for toggling the pileup view
+    // @undoBatch
+    // onClick = (e: React.MouseEvent) => {
+    //     if (e.button === 0) {
+    //         SelectionManager.DeselectAll();
+    //         this.toggleStarburst();
+    //         e.stopPropagation();
+    //     }
+    // };
+
+    render() {
+        return (
+            <div className={`collectionPileView`} 
+            // onClick={this.onClick} 
+            onPointerDown={this.pointerDown} style={{ width: this._props.PanelWidth(), height: '100%' }}>
+                {this.contents}
+            </div>
+        );
+    }
+}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 18eb4dd1f..b7805bf3f 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -33,6 +33,8 @@ import { CollectionLinearView } from './collectionLinear';
 import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
 import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
 import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView';
+import { CollectionCardView } from './CollectionCardDeckView';
+
 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)
@@ -134,6 +136,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
             case CollectionViewType.Masonry:      return <CollectionStackingView    key="collview" {...props} />;
             case CollectionViewType.Time:         return <CollectionTimeView        key="collview" {...props} />;
             case CollectionViewType.Grid:         return <CollectionGridView        key="collview" {...props} />;
+            case CollectionViewType.Card:         return <CollectionCardView        key="collview" {...props} />;
+
+
         }
     };
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index b8c0967c1..e972f44f1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -126,6 +126,40 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
     return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
 }
 
+export function computeCardDeckLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+    const docMap = new Map<string, PoolData>();
+    const spreadWidth = Math.min(panelDim[0], childPairs.length * 50); // Total width of the spread
+    const startX = -(spreadWidth / 2); // Starting X position
+    const fanAngle = 5; // Angle in degrees for fanning out cards
+    const baseZIndex = 1000; // Base Z-index to ensure cards are stacked in order
+
+    childPairs.forEach(({ layout, data }, i) => {
+        const aspect = NumCast(layout._height) / NumCast(layout._width);
+        const docSize = Math.min(400, NumCast(layout._width)) * NumCast(pivotDoc._starburstDocScale, 1);
+        const posX = startX + (spreadWidth / childPairs.length) * i;
+        const posY = 0; // Adjust if you want to change the vertical alignment
+        const rotation = (i - (childPairs.length / 2)) * fanAngle; // Calculate rotation for fanning effect
+
+        docMap.set(layout[Id], {
+            x: posX,
+            y: posY,
+            width: docSize,
+            height: docSize * aspect,
+            zIndex: baseZIndex + i, // Increment Z-index for each card to stack them correctly
+            rotation: rotation, // Optional: Add this if you want to rotate elements for a fanned effect
+            pair: { layout, data },
+            replica: '',
+            color: 'white',
+            backgroundColor: 'white',
+            transition: 'all 0.3s',
+        });
+    });
+
+    // This is a placeholder for the divider object and may need to be adjusted based on actual usage
+    const divider = { type: 'div', color: 'transparent', x: -panelDim[0] / 2, y: -panelDim[1] / 2, width: 15, height: 15, payload: undefined };
+    return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
+}
+
 export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const docMap = new Map<string, PoolData>();
     const fieldKey = 'data';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1fd453e96..f9fe306fa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -49,7 +49,7 @@ import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
 import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
-import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult, computeCardDeckLayout } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
@@ -1383,6 +1383,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             case computeTimelineLayout.name:  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
             case computePivotLayout.name:     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
             case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
+            case computeCardDeckLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeCardDeckLayout) };
+
         }
         return { newPool, computedElementData: this.doFreeformLayout(newPool) };
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index b913e05ad..d0ac5f6db 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -383,6 +383,18 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this.hideMarquee();
     });
 
+    @undoBatch
+    spreadCards = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
+        const selected = this.marqueeSelect(false);
+        SelectionManager.DeselectAll();
+        selected.forEach(d => this._props.removeDocument?.(d));
+        const newCollection = DocUtils.spreadCards(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!;
+        this._props.addDocument?.(newCollection);
+        this._props.selectDocuments([newCollection]);
+        MarqueeOptionsMenu.Instance.fadeOut(true);
+        this.hideMarquee();
+    });
+
     /**
      * This triggers the TabDocView.PinDoc method which is the universal method
      * used to pin documents to the currently active presentation trail.
@@ -508,6 +520,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
 
     @action
     marqueeCommand = (e: KeyboardEvent) => {
+
         if (this._commandExecuted || (e as any).propagationIsStopped) {
             return;
         }
@@ -518,7 +531,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             this.delete(e, e.key === 'h');
             e.stopPropagation();
         }
-        if ('ctsSpg'.indexOf(e.key) !== -1) {
+        if ('ctsSpga'.indexOf(e.key) !== -1) {
+
             this._commandExecuted = true;
             e.stopPropagation();
             e.preventDefault();
@@ -526,7 +540,9 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             if (e.key === 'g') this.collection(e, true);
             if (e.key === 'c' || e.key === 't') this.collection(e);
             if (e.key === 's' || e.key === 'S') this.summary(e);
-            if (e.key === 'p') this.pileup(e);
+            if (e.key === 'p') this.pileup(e)
+            if (e.key === 'a') this.spreadCards(e);
+
             this.cleanupInteractions(false);
         }
         if (e.key === 'r' || e.key === ' ') {
-- 
cgit v1.2.3-70-g09d2


From 5ff0bef5d3c4825aa7210a26c98aae3b24f4a835 Mon Sep 17 00:00:00 2001
From: alyssaf16 <alyssa_feinberg@brown.edu>
Date: Fri, 17 May 2024 13:18:40 -0400
Subject: chatcards, quizcards, and ai flashcards

---
 src/client/apis/gpt/GPT.ts                         |  36 ++++--
 src/client/util/CurrentUserUtils.ts                |   2 -
 .../views/collections/CollectionCarouselView.tsx   |  79 ++++++++++--
 src/client/views/collections/CollectionView.tsx    |  15 ++-
 src/client/views/nodes/ComparisonBox.scss          | 143 +++++++++++++++++++++
 src/client/views/nodes/ComparisonBox.tsx           | 109 +++++++++++-----
 src/client/views/nodes/DocumentView.tsx            |  24 +++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   5 +-
 src/client/views/pdf/AnchorMenu.tsx                |  27 ++--
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         |  45 -------
 src/client/views/pdf/PDFViewer.tsx                 |   4 +-
 11 files changed, 367 insertions(+), 122 deletions(-)

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

diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 2757fc830..6600ddab2 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -1,10 +1,13 @@
 import { ClientOptions, OpenAI } from 'openai';
+import { ChatCompletionMessageParam } from 'openai/resources';
 
 enum GPTCallType {
     SUMMARY = 'summary',
     COMPLETION = 'completion',
     EDIT = 'edit',
+    CHATCARD = 'chatcard',
     FLASHCARD = 'flashcard',
+    QUIZ = 'quiz',
 }
 
 type GPTCallOpts = {
@@ -15,10 +18,17 @@ type GPTCallOpts = {
 };
 
 const callTypeMap: { [type: string]: GPTCallOpts } = {
-    summary: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
-    edit: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
-    flashcard: { model: 'gpt-3.5-turbo-instruct', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with questions and answers: ' },
-    completion: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: '' },
+    summary: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
+    edit: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
+    flashcard: { model: 'gpt-4-turbo', maxTokens: 512, temp: 0.5, prompt: 'Make flashcards out of this text with each question and answer labeled. Do not label each flashcard and do not include asterisks: ' },
+    completion: { model: 'gpt-4-turbo', maxTokens: 256, temp: 0.5, prompt: '' },
+    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: {
+        model: 'gpt-4-turbo',
+        maxTokens: 1024,
+        temp: 0,
+        prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct',
+    },
 };
 
 /**
@@ -28,7 +38,8 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
  * @returns AI Output
  */
 const gptAPICall = async (inputText: string, callType: GPTCallType) => {
-    if (callType === GPTCallType.SUMMARY || callType == GPTCallType.FLASHCARD) inputText += '.';
+    if (!inputText) return 'Please provide a response.';
+    if (callType === GPTCallType.SUMMARY || callType == GPTCallType.FLASHCARD || GPTCallType.QUIZ) inputText += '.';
     const opts: GPTCallOpts = callTypeMap[callType];
     try {
         const configuration: ClientOptions = {
@@ -36,13 +47,20 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
             dangerouslyAllowBrowser: true,
         };
         const openai = new OpenAI(configuration);
-        const response = await openai.completions.create({
+
+        let messages: ChatCompletionMessageParam[] = [
+            { role: 'system', content: opts.prompt },
+            { role: 'user', content: inputText },
+        ];
+
+        const response = await openai.chat.completions.create({
             model: opts.model,
-            max_tokens: opts.maxTokens,
+            messages: messages,
             temperature: opts.temp,
-            prompt: `${opts.prompt}${inputText}`,
+            max_tokens: opts.maxTokens,
         });
-        return response.choices[0].text;
+
+        return response.choices[0].message.content;
     } 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 ab0315ba1..b6ba3f187 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -342,8 +342,6 @@ pie title Minerals in my tap water
               }[] = [
             {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, _width: 300, _height: 300}},
-            // {key: "Flashcard",  creator: Docs.Create.ComparisonDocument("", opts),           opts: { _layout_isFlashcard: true, _width: 300, _height: 300 }},
-            //{key: "Flashcard",  creator: opts => Docs.Create.TextDocument("", opts),        opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}},
             {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, }},
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 7f5176123..d45b0822b 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -33,29 +33,53 @@ export class CollectionCarouselView extends CollectionSubView() {
         }
     };
 
+    /**
+     * Goes to the next flashcard in the stack and filters
+     * based on the the currently selected option.
+     */
     advance = (e: React.MouseEvent) => {
         e.stopPropagation();
         this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length;
         var startInd = this.layoutDoc._carousel_index;
 
         // if the star filter is selected
-        if (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'star') {
-            // go to a new index that is starred, skip the ones that aren't
+        if (this.layoutDoc[`filterOp`] == 'star') {
+            // go to a flashcard that is starred, skip the ones that aren't
             while (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`] && (startInd + 1) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) {
                 startInd = (startInd + 1) % this.childLayoutPairs.length;
             }
             this.layoutDoc._carousel_index = startInd;
+            // if there aren't any starred, show all cards
+            if (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`]) {
+                this.layoutDoc[`filterOp`] = 'all';
+            }
         }
 
         // if the practice filter is selected
-        if (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'practice') {
+        if (this.layoutDoc[`filterOp`] == 'practice') {
             // go to a new index that is missed, skip the ones that are correct
             while (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct' && (startInd + 1) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) {
                 startInd = (startInd + 1) % this.childLayoutPairs.length;
             }
             this.layoutDoc._carousel_index = startInd;
+
+            // if the user has gone through all of the cards and gotten them all correct, show all cards and exit practice mode
+            if (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct') {
+                this.layoutDoc[`filterOp`] = 'all';
+
+                // set all the cards to missed
+                for (var i = 0; i < this.childLayoutPairs.length; i++) {
+                    const curDoc = this.childLayoutPairs?.[NumCast(i)];
+                    curDoc.layout[`${this.fieldKey}_missed`] = undefined;
+                }
+            }
         }
     };
+
+    /**
+     * Goes to the previous flashcard in the stack and filters
+     * based on the the currently selected option.
+     */
     goback = (e: React.MouseEvent) => {
         e.stopPropagation();
         this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
@@ -63,38 +87,58 @@ export class CollectionCarouselView extends CollectionSubView() {
         var startInd = this.layoutDoc._carousel_index;
 
         // if the star filter is selected
-        if (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'star') {
+        if (this.layoutDoc[`filterOp`] == 'star') {
             // go to a new index that is starred, skip the ones that aren't
             while (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`] && (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) {
                 startInd = (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
             }
             this.layoutDoc._carousel_index = startInd;
+            // if there aren't any starred, show all cards
+            if (!this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_star`]) {
+                this.layoutDoc[`filterOp`] = 'all';
+            }
         }
 
         // if the practice filter is selected
-        if (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'practice') {
+        if (this.layoutDoc[`filterOp`] == 'practice') {
             // go to a new index that is missed, skip the ones that are correct
             while (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct' && (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length != this.layoutDoc._carousel_index) {
                 startInd = (startInd - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
             }
+
             this.layoutDoc._carousel_index = startInd;
+
+            // See all flashcards when finish going through practice mode and set all of the flashcards back to
+            if (this.childLayoutPairs?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] == 'correct') {
+                this.layoutDoc[`filterOp`] = 'all';
+
+                for (var i = 0; i < this.childLayoutPairs.length; i++) {
+                    const curDoc = this.childLayoutPairs?.[NumCast(i)];
+                    curDoc.layout[`${this.fieldKey}_missed`] = undefined;
+                }
+            }
         }
     };
 
+    /*
+     * Stars the document when the star button is pressed.
+     */
     star = (e: React.MouseEvent) => {
         e.stopPropagation();
-        // stars the document when the button is pressed
         const curDoc = this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)];
+        if (!curDoc) return;
         if (curDoc.layout[`${this.fieldKey}_star`] == undefined) curDoc.layout[`${this.fieldKey}_star`] = true;
         else curDoc.layout[`${this.fieldKey}_star`] = !curDoc.layout[`${this.fieldKey}_star`];
     };
 
+    /*
+     * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode.
+     */
     missed = (e: React.MouseEvent, val: string) => {
         e.stopPropagation();
         const curDoc = this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)];
         curDoc.layout[`${this.fieldKey}_missed`] = val;
-        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length;
-        this.advance;
+        this.advance(e);
     };
 
     captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => {
@@ -154,6 +198,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         );
     }
     @computed get buttons() {
+        if (!this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)]) return;
         return (
             <>
                 <div key="back" className="carouselView-back" onClick={this.goback}>
@@ -165,10 +210,10 @@ export class CollectionCarouselView extends CollectionSubView() {
                 <div key="star" className="carouselView-star" onClick={this.star}>
                     <FontAwesomeIcon icon={'star'} color={this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)].layout[`${this.fieldKey}_star`] ? 'yellow' : 'gray'} size={'1x'} />
                 </div>
-                <div key="remove" className="carouselView-remove" onClick={e => this.missed(e, 'missed')} style={{ visibility: this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'practice' ? 'visible' : 'hidden' }}>
+                <div key="remove" className="carouselView-remove" onClick={e => this.missed(e, 'missed')} style={{ visibility: this.layoutDoc[`filterOp`] == 'practice' ? 'visible' : 'hidden' }}>
                     <FontAwesomeIcon icon={'xmark'} color={'red'} size={'1x'} />
                 </div>
-                <div key="check" className="carouselView-check" onClick={e => this.missed(e, 'correct')} style={{ visibility: this.layoutDoc[`_${this._props.fieldKey}_filterOp`] == 'practice' ? 'visible' : 'hidden' }}>
+                <div key="check" className="carouselView-check" onClick={e => this.missed(e, 'correct')} style={{ visibility: this.layoutDoc[`filterOp`] == 'practice' ? 'visible' : 'hidden' }}>
                     <FontAwesomeIcon icon={'check'} color={'green'} size={'1x'} />
                 </div>
             </>
@@ -185,6 +230,18 @@ export class CollectionCarouselView extends CollectionSubView() {
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color),
                 }}>
                 {this.content}
+                {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */}
+                <p
+                    style={{
+                        display: !this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc[`filterOp`] == 'practice' ? 'flex' : 'none',
+                        justifyContent: 'center',
+                        alignItems: 'center',
+                        height: '100%',
+                        zIndex: '-1',
+                    }}>
+                    Add flashcards!
+                </p>
+                {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */}
                 <p
                     style={{
                         color: 'red',
@@ -192,7 +249,7 @@ export class CollectionCarouselView extends CollectionSubView() {
                         position: 'relative',
                         left: '10px',
                         top: '10px',
-                        visibility: this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)].layout[`${this.fieldKey}_missed`] == 'missed' ? 'visible' : 'hidden',
+                        display: this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)] ? (this.childLayoutPairs?.[NumCast(this.layoutDoc._carousel_index)].layout[`${this.fieldKey}_missed`] == 'missed' ? 'block' : 'none') : 'none',
                     }}>
                     Recently missed!
                 </p>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 168176edf..2f0f2a773 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -80,6 +80,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         super(props);
         makeObservable(this);
         this._annotationKeySuffix = returnEmptyString;
+        this.layoutDoc[`filterOp`] = 'all';
     }
 
     componentDidMount() {
@@ -176,14 +177,18 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                     return newRendition;
                 });
 
+            // creates menu option for flashcard filters
             const revealOptions = cm.findByDescription('Filter Flashcards');
             const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
-            revealItems.push({ description: 'All', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'all'), icon: 'eye-slash' });
-            revealItems.push({ description: 'Star', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'star'), icon: 'hand-point-up' });
-            revealItems.push({ description: 'Practice Mode', event: () => (this.layoutDoc[`_${this._props.fieldKey}_filterOp`] = 'practice'), icon: 'rotate' });
+            revealItems.push({ description: 'All', event: () => (this.layoutDoc[`filterOp`] = 'all'), icon: 'layer-group' });
+            revealItems.push({ description: 'Star', event: () => (this.layoutDoc[`filterOp`] = 'star'), icon: 'star' });
+            revealItems.push({ description: 'Practice Mode', event: () => (this.layoutDoc[`filterOp`] = 'practice'), icon: 'check' });
+            revealItems.push({ description: 'Quiz Cards', event: () => (this.layoutDoc[`filterOp`] = 'quiz'), icon: 'pencil' });
 
-            //revealItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
-            !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+            // only show the filter options if it is a collection of type Carousel view
+            if (this.Document?._type_collection === CollectionViewType.Carousel) {
+                !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+            }
 
             const options = cm.findByDescription('Options...');
             const optionItems = options && 'subitems' in options ? options.subitems : [];
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 39c864b2b..093b9c004 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -1,4 +1,5 @@
 .comparisonBox-interactive,
+.quiz-card,
 .comparisonBox {
     border-radius: inherit;
     width: 100%;
@@ -7,6 +8,40 @@
     z-index: 0;
     pointer-events: none;
     display: flex;
+    p {
+        color: rgb(0, 0, 0);
+        -webkit-text-stroke-color: black;
+        -webkit-text-stroke-width: 0.2px;
+    }
+
+    .input-box {
+        position: relative;
+        padding: 10px;
+        width: 100%;
+        height: 100%;
+        display: flex;
+    }
+
+    .submit-button {
+        position: relative;
+        padding-bottom: 10px;
+        padding-left: 5px;
+        padding-right: 5px;
+        width: 100%;
+        height: 15%;
+        display: flex;
+
+        button {
+            flex: 1;
+            position: relative;
+        }
+    }
+    textarea {
+        flex: 1;
+        padding: 10px;
+        position: relative;
+        resize: none;
+    }
 
     .clip-div {
         position: absolute;
@@ -95,4 +130,112 @@
             display: flex;
         }
     }
+    // .input-box {
+    //     position: relative;
+    //     padding: 10px;
+    // }
+    // input[type='text'] {
+    //     flex: 1;
+    //     position: relative;
+    //     margin-right: 10px;
+    //     width: 100px;
+    // }
+}
+
+// .quiz-card {
+//     position: relative;
+
+//     input[type='text'] {
+//         flex: 1;
+//         position: relative;
+//         margin-right: 10px;
+//         width: 100px;
+//     }
+// }
+.QuizCard {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+
+    .QuizCard-wrapper {
+        width: 100%;
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        .QuizCardBox {
+            /* existing code */
+
+            .DIYNodeBox-iframe {
+                height: 100%;
+                width: 100%;
+                border: none;
+            }
+        }
+
+        .search-bar {
+            display: flex;
+            justify-content: left;
+            align-items: left;
+            width: 100%;
+            padding: 10px;
+
+            input[type='text'] {
+                flex: 1;
+                margin-right: 10px;
+            }
+
+            button {
+                padding: 5px 10px;
+            }
+        }
+
+        .content {
+            flex: 1;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            width: 100%;
+            height: 100%;
+            .diagramBox {
+                flex: 1;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                width: 100%;
+                height: 100%;
+                svg {
+                    flex: 1;
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    width: 100%;
+                    height: 100%;
+                }
+            }
+        }
+
+        .loading-circle {
+            position: relative;
+            width: 50px;
+            height: 50px;
+            border-radius: 50%;
+            border: 3px solid #ccc;
+            border-top-color: #333;
+            animation: spin 1s infinite linear;
+        }
+
+        @keyframes spin {
+            0% {
+                transform: rotate(0deg);
+            }
+            100% {
+                transform: rotate(360deg);
+            }
+        }
+    }
 }
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 19fccce8a..9fd4d696a 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents, unimplementedFunction } from '../../../Utils';
 import { Doc, Opt, DocListCast } from '../../../fields/Doc';
 import { DocCast, NumCast, RTFCast, StrCast } from '../../../fields/Types';
 import { DocUtils, Docs } from '../../documents/Documents';
@@ -34,6 +34,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         makeObservable(this);
     }
 
+    @observable inputValue = '';
+    @observable outputValue = '';
+    @observable loading = false;
+    @observable errorMessage = '';
+    @observable outputMessage = '';
+
+    @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+        this.inputValue = e.target.value;
+        console.log(this.inputValue);
+    };
+
     @observable _animating = '';
 
     @computed get clipWidth() {
@@ -160,7 +171,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             e => {
                 const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], dropActionType.move);
                 de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
-                    //this.clearDoc(which);
                     return addDocument(doc);
                 };
                 de.canEmbed = true;
@@ -181,15 +191,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
     _closeRef = React.createRef<HTMLDivElement>();
 
+    /**
+     * Flips a flashcard to the alternate side for the user to view.
+     */
     flipFlashcard = () => {
         const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
-
         this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : undefined;
     };
+
+    /**
+     * Changes the view option to hover for a flashcard.
+     */
     hoverFlip = (side: string | undefined) => {
         if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] == 'hover') this.layoutDoc[`_${this._props.fieldKey}_usePath`] = side;
     };
 
+    /**
+     * Creates the button used to flip the flashcards.
+     */
     @computed get overlayAlternateIcon() {
         const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`];
         return (
@@ -203,21 +222,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                                 this.flipFlashcard();
                                 console.log('Print Front of cards: ' + RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
                                 console.log('Print Back of cards: ' + RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
-
-                                //const queryText = RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text;
-                                // DocCast(this.dataDoc[this.fieldKey + '_1'])[DocData].text = 'hello';
-                                // const mes = gptAPICall(queryText, GPTCallType.COMPLETION).trim();
-                                // const res = await gptAPICall(queryText, GPTCallType.COMPLETION)
-                                // console.log(res);
-                                //.then(value => (DocCast(this.dataDoc[this.fieldKey + '_1']).text = value.trim()));
-                                if (usepath !== 'alternate') {
-                                    this.askGPT();
-                                }
                             }
                         })
                     }
                     style={{
-                        //display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none',
                         background: usepath === 'alternate' ? 'white' : 'black',
                         color: usepath === 'alternate' ? 'black' : 'white',
                     }}>
@@ -227,15 +235,34 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         );
     }
 
+    @action handleRenderGPTClick = () => {
+        // Call the GPT model and get the output
+        this.layoutDoc[`_${this._props.fieldKey}_usePath`] = 'alternate';
+        this.outputValue = '';
+        if (this.inputValue) this.askGPT();
+    };
+
+    @action handleRenderClick = () => {
+        // Call the GPT model and get the output
+        this.layoutDoc[`_${this._props.fieldKey}_usePath`] = undefined;
+    };
+
+    /**
+     * Calls the GPT model to create QuizCards. Evaluates how similar the user's response is to the alternate
+     * side of the flashcard.
+     */
     askGPT = async (): Promise<string | undefined> => {
-        const queryText = RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text;
+        const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+        const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
+        const queryText = questionText + ' UserAnswer: ' + this.inputValue + '. ' + rubricText;
+
         try {
-            let res = await gptAPICall(StrCast(queryText), GPTCallType.COMPLETION);
+            let res = await gptAPICall(queryText, GPTCallType.QUIZ);
             if (!res) {
                 console.error('GPT call failed');
                 return;
             }
-            DocCast(this.dataDoc[this.fieldKey + '_0'])[DocData].text = res;
+            this.outputValue = res;
             console.log(res);
         } catch (err) {
             console.error('GPT call failed');
@@ -292,8 +319,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             );
         };
         const displayBox = (which: string, index: number, cover: number) => {
-            // if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] == 'hide/reveal') this.layoutDoc[this.clipHeightKey] = 100;
-            // else this.layoutDoc.height = 300;
             return (
                 <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this._props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
                     {displayDoc(which)}
@@ -326,36 +351,54 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
 
         if (this.Document._layout_isFlashcard) {
             const side = this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 1 : 0;
-            // add text box when first created
 
+            // add text box to each side when comparison box is first created
             if (!(this.dataDoc[this.fieldKey + '_0'] || this.dataDoc[this.fieldKey + '_0'] == 'empty')) {
                 const dataSplit = StrCast(this.dataDoc.data).split('Answer');
                 const newDoc = Docs.Create.TextDocument(dataSplit[1]);
+                // if there is text from the pdf ai cards, put the question on the front side.
                 newDoc[DocData].text = dataSplit[1];
                 this.addDoc(newDoc, this.fieldKey + '_0');
             }
             if (!(this.dataDoc[this.fieldKey + '_1'] || this.dataDoc[this.fieldKey + '_1'] == 'empty')) {
                 const dataSplit = StrCast(this.dataDoc.data).split('Answer');
                 const newDoc = Docs.Create.TextDocument(dataSplit[0]);
-                newDoc[DocData].text = 'placeholder...';
+                // if there is text from the pdf ai cards, put the answer on the alternate side.
+                newDoc[DocData].text = dataSplit[0];
                 this.addDoc(newDoc, this.fieldKey + '_1');
             }
 
-            if (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] == 'hide/reveal') {
-                {
-                    return (
-                        <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}>
-                            {displayBox(`${this.fieldKey}_0`, side, this._props.PanelHeight() - 3)}
-                            {displayBox(`${this.fieldKey}_1`, 1, this._props.PanelHeight() - 3)}
+            // render the QuizCards
+            if (DocCast(this.Document.embedContainer) && DocCast(this.Document.embedContainer)[`filterOp`] == 'quiz') {
+                return (
+                    <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} style={{ display: 'flex', flexDirection: 'column' }}>
+                        <p style={{ color: 'white', padding: 10 }}>{StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)}</p>
+                        {/* {StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text)} */}
+                        <div className={'input-box'}>
+                            {
+                                <textarea
+                                    value={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? this.outputValue : this.inputValue}
+                                    onChange={this.handleInputChange}
+                                    readOnly={this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate'}
+                                />
+                            }
                         </div>
-                    );
-                }
-            } else {
+                        <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'none' : 'flex' }}>
+                            <button onClick={this.handleRenderGPTClick}>Submit</button>
+                        </div>
+                        <div className="submit-button" style={{ display: this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate' ? 'flex' : 'none' }}>
+                            <button onClick={this.handleRenderClick}>Edit Your Response</button>
+                        </div>
+                    </div>
+                );
+            }
+
+            // render a normal flashcard when not a QuizCard
+            else {
                 return (
                     <div
                         className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
-                        // style={{ display: 'flex', flexDirection: 'column' }}
-                        // style={{ position: 'absolute', top: '0px' }}
+                        style={{ display: 'flex', flexDirection: 'column' }}
                         onMouseEnter={() => {
                             this.hoverFlip('alternate');
                         }}
@@ -363,12 +406,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
                             this.hoverFlip(undefined);
                         }}>
                         {displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
-
                         {this.overlayAlternateIcon}
                     </div>
                 );
             }
         } else {
+            // render a comparison box that compares items side by side
             return (
                 <div className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
                     {displayBox(`${this.fieldKey}_2`, 1, this._props.PanelWidth() - 3)}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 26e5ec622..e1b501c5a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -48,6 +48,9 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { FieldsDropdown } from '../FieldsDropdown';
+import { RTFCast } from '../../../fields/Types';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+
 interface Window {
     MediaRecorder: MediaRecorder;
 }
@@ -517,6 +520,21 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         input.click();
     };
 
+    askGPT = async (): Promise<string | undefined> => {
+        const queryText = RTFCast(DocCast(this.dataDoc[this.props.fieldKey + '_1']).text)?.Text;
+        try {
+            let res = await gptAPICall('Question: ' + StrCast(queryText), GPTCallType.CHATCARD);
+            if (!res) {
+                console.error('GPT call failed');
+                return;
+            }
+            DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
+            console.log(res);
+        } catch (err) {
+            console.error('GPT call failed');
+        }
+    };
+
     onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
             e.preventDefault();
@@ -569,17 +587,19 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                 appearanceItems.splice(0, 0, { description: 'Open in Lightbox', event: () => LightboxView.Instance.SetLightboxDoc(this.Document), icon: 'external-link-alt' });
             }
             appearanceItems.push({ description: 'Pin', event: () => this._props.pinToPres(this.Document, {}), icon: 'eye' });
+            if (this.Document._layout_isFlashcard) {
+                appearanceItems.push({ description: 'Create ChatCard', event: () => this.askGPT(), icon: 'id-card' });
+            }
 
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this._props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'compass' });
 
+            // creates menu for the user to select how to reveal the flashcards
             if (this.Document._layout_isFlashcard) {
                 const revealOptions = cm.findByDescription('Reveal Options');
                 const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
                 revealItems.push({ description: 'Hover', event: () => (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover'), icon: 'hand-point-up' });
                 revealItems.push({ description: 'Flip', event: () => (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip'), icon: 'rotate' });
-                revealItems.push({ description: 'Hide & Reveal', event: () => (this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hide/reveal'), icon: 'eye-slash' });
-                //revealItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
                 !revealOptions && cm.addItem({ description: 'Reveal Options', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
             }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4fce72cdc..3192ac537 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -178,6 +178,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     @observable
     private gptRes: string = '';
 
+    public makeAIFlashcards: () => void = unimplementedFunction;
+    public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
+
     public static PasteOnLoad: ClipboardEvent | undefined;
     private static SelectOnLoad: Doc | undefined;
     public static SetSelectOnLoad(doc: Doc) {
@@ -959,7 +962,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const options = cm.findByDescription('Options...');
         const optionItems = options && 'subitems' in options ? options.subitems : [];
         optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
-        optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
+        optionItems.push({ description: `Ask GPT-3`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' });
         this._props.renderDepth &&
             optionItems.push({
                 description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 7cb6a20f4..a0c3cf487 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -76,7 +76,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
      * @param e pointer down event
      */
     gptSummarize = async (e: React.PointerEvent) => {
-        // move this logic to gptpopup, need to implement generate again
         GPTPopup.Instance.setVisible(true);
         GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
         GPTPopup.Instance.setLoading(true);
@@ -90,24 +89,28 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
         GPTPopup.Instance.setLoading(false);
     };
 
+    /**
+     * Invokes the API with the selected text and stores it in the selected text.
+     * @param e pointer down event
+     */
     gptFlashcards = async (e: React.PointerEvent) => {
-        // move this logic to gptpopup, need to implement generate again
-        // GPTPopup.Instance.setVisible(true);
-        // GPTPopup.Instance.setMode(GPTPopupMode.FLASHCARD);
-        // GPTPopup.Instance.setLoading(true);
-
+        const queryText = this.selectedText;
         try {
-            const res = await gptAPICall(this.selectedText, GPTCallType.FLASHCARD);
-
+            const res = await gptAPICall(queryText, GPTCallType.FLASHCARD);
+            console.log(res);
             GPTPopup.Instance.setText(res || 'Something went wrong.');
-            this.transferToFlashcard(res);
+            this.transferToFlashcard(res || 'Something went wrong');
         } catch (err) {
             console.error(err);
         }
         GPTPopup.Instance.setLoading(false);
     };
 
+    /*
+     * Transfers the flashcard text generated by GPT on flashcards and creates a collection out them.
+     */
     transferToFlashcard = (text: string) => {
+        // put each question generated by GPT on the front of the flashcard
         const senArr = text.split('Question');
         const collectionArr: Doc[] = [];
         for (var i = 1; i < senArr.length; i++) {
@@ -116,10 +119,11 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
             newDoc.text = senArr[i];
             collectionArr.push(newDoc);
         }
+        // create a new carousel collection of these flashcards
         const newCol = Docs.Create.CarouselDocument(collectionArr, {
-            _width: 200,
+            _width: 250,
             _height: 200,
-            _layout_fitWidth: true,
+            _layout_fitWidth: false,
             _layout_autoHeight: true,
         });
 
@@ -215,6 +219,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                             color={SettingsManager.userColor}
                         />
                     )}
+                    {/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */}
                     <IconButton
                         tooltip="Create flashcards" //
                         onPointerDown={this.gptFlashcards}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 32c1721ec..0b741c85e 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -144,15 +144,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
             _layout_autoHeight: true,
         });
         this.addDoc(newDoc, this.sidebarId);
-        // const arr = [newDoc];
-        // const newCol = Docs.Create.CarouselDocument(arr, {
-        //     _width: 200,
-        //     _height: 200,
-        //     _layout_fitWidth: true,
-        //     _layout_autoHeight: true,
-        // });
-        // this.addDoc(newDoc, this.sidebarId);
-        // this.addDoc(newCol, this.sidebarId);
         const anchor = AnchorMenu.Instance?.GetAnchor(undefined, false);
         if (anchor) {
             DocUtils.MakeLink(newDoc, anchor, {
@@ -161,25 +152,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
         }
     };
 
-    // transferToFlashcard = () => {
-    //     const senArr = this.text.split('Question');
-    //     const collectionArr: Doc[] = [];
-    //     for (var i = 1; i < senArr.length; i++) {
-    //         console.log('Arr ' + i + ': ' + senArr[i]);
-    //         const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
-    //         newDoc.text = senArr[i];
-    //         collectionArr.push(newDoc);
-    //     }
-    //     const newCol = Docs.Create.CarouselDocument(collectionArr, {
-    //         _width: 200,
-    //         _height: 200,
-    //         _layout_fitWidth: true,
-    //         _layout_autoHeight: true,
-    //     });
-    //     this.addDoc(newCol, this.sidebarId);
-    //     this.addToCollection?.(newCol);
-    // };
-
     /**
      * Transfers the image urls to actual image docs
      */
@@ -244,23 +216,6 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
         );
     };
 
-    flashcardBox = () => {
-        // const textArr = this.text.split(".");
-        // textArr.forEach(function(sentence) {
-        //     console.log(sentence);
-
-        // });
-        // const newDoc = Docs.Create.ComparisonDocument();
-        // this.addToCollection?.(newDoc);
-        // // const newDoc = Docs.Create.ComparisonDocument();
-        // DocUtils.copyDragFactory(Doc.UserDoc().emptyFlashcard as Doc);
-        // // this.addToCollection?.(newDoc);
-        // // return newDoc;
-        // <ComparisonBox/>
-        const newDoc = Docs.Create.TextDocument('Hello there');
-        this.addDoc?.(newDoc);
-    };
-
     data = () => {
         return (
             <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index cecaf17ff..fe1ed8159 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -414,12 +414,10 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
             AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
         }
 
-        // Changing which document to add the annotation to (the currently selected PDF)
         GPTPopup.Instance.setSidebarId('data_sidebar');
         GPTPopup.Instance.addDoc = this._props.sidebarAddDoc;
+        // allows for creating collection
         AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
-        // const newDoc = Docs.Create.ComparisonDocument({ _layout_isFlashcard: true, _width: 300, _height: 300 });
-        // this.props.addDocument?.(newDoc);
     };
 
     @action
-- 
cgit v1.2.3-70-g09d2


From 9badfc489f00afd10489f0dde0a3b9e21817feb5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 18 May 2024 23:15:49 -0400
Subject: more cardView cleanup

---
 src/Utils.ts                                       |   1 -
 src/client/apis/gpt/GPT.ts                         |  22 ++--
 src/client/documents/Documents.ts                  |  65 +----------
 src/client/util/SelectionManager.ts                |   1 -
 src/client/views/DocComponent.tsx                  |   2 +-
 .../views/collections/CollectionCardDeckView.tsx   |  20 ++--
 src/client/views/collections/CollectionMenu.tsx    |   2 -
 src/client/views/collections/CollectionView.tsx    |   2 -
 .../CollectionFreeFormLayoutEngines.tsx            |  26 -----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   8 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  19 +---
 .../collectionSchema/CollectionSchemaView.tsx      |   2 +-
 src/client/views/global/globalScripts.ts           |  32 +++---
 src/client/views/nodes/PDFBox.tsx                  |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   7 +-
 src/client/views/pdf/GPTPopup/GPTPopup.tsx         | 126 +++++++++------------
 16 files changed, 105 insertions(+), 233 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index fb72a5836..291d7c799 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -179,7 +179,6 @@ export namespace Utils {
     export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
         return 'rgba(' + col.r + ',' + col.g + ',' + col.b + (col.a !== undefined ? ',' + col.a : '') + ')';
     }
-    
 
     export function HSLtoRGB(h: number, s: number, l: number) {
         // Must be fractions of 1
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 398f8ae39..3b3b3b9a0 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -6,7 +6,7 @@ enum GPTCallType {
     COMPLETION = 'completion',
     EDIT = 'edit',
     SORT = 'sort',
-    DESCRIBE = 'describe'
+    DESCRIBE = 'describe',
 }
 
 type GPTCallOpts = {
@@ -20,13 +20,15 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
     summary: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
     edit: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
     completion: { model: 'gpt-3.5-turbo-instruct', maxTokens: 256, temp: 0.5, prompt: '' },
-    sort:{model:'gpt-4o',maxTokens:2048,temp:0.5,prompt:"I'm going to give you a list of descriptions. Each one is seperated by ====== on either side. They will vary in length, so make sure to only seperate when you see ======. Sort them into lists by shared content. MAKE SURE EACH DESCRIPTOR IS IN ONLY ONE LIST. Generate only the list with each list seperated by ====== with the elements seperated by ~~~~~~. Try to do around 4 groups, but a little more or less is ok."},
-    describe:{model:'gpt-4-vision-preview',maxTokens:2048,temp:0,prompt:"Describe these images in 3-5 words"},
-
-
+    sort: {
+        model: 'gpt-4o',
+        maxTokens: 2048,
+        temp: 0.5,
+        prompt: "I'm going to give you a list of descriptions. Each one is seperated by ====== on either side. They will vary in length, so make sure to only seperate when you see ======. Sort them into lists by shared content. MAKE SURE EACH DESCRIPTOR IS IN ONLY ONE LIST. Generate only the list with each list seperated by ====== with the elements seperated by ~~~~~~. Try to do around 4 groups, but a little more or less is ok.",
+    },
+    describe: { model: 'gpt-4-vision-preview', maxTokens: 2048, temp: 0, prompt: 'Describe these images in 3-5 words' },
 };
 
-
 /**`
  * Calls the OpenAI API.
  *
@@ -39,7 +41,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
     const opts: GPTCallOpts = callTypeMap[callType];
     try {
         const configuration: ClientOptions = {
-            apiKey: "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",
+            apiKey: process.env.OPENAI_KEY,
             dangerouslyAllowBrowser: true,
         };
         const openai = new OpenAI(configuration);
@@ -63,11 +65,10 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
     }
 };
 
-
 const gptImageLabel = async (imgUrl: string): Promise<string> => {
     try {
         const configuration: ClientOptions = {
-            apiKey: "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",
+            apiKey: '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',
             dangerouslyAllowBrowser: true,
         };
 
@@ -100,7 +101,6 @@ const gptImageLabel = async (imgUrl: string): Promise<string> => {
     }
 };
 
-
 const gptImageCall = async (prompt: string, n?: number) => {
     try {
         const configuration: ClientOptions = {
@@ -122,4 +122,4 @@ const gptImageCall = async (prompt: string, n?: number) => {
     }
 };
 
-export { gptAPICall, gptImageCall, gptImageLabel, GPTCallType };
\ No newline at end of file
+export { gptAPICall, gptImageCall, gptImageLabel, GPTCallType };
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 80774f4ad..903bd907a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -500,27 +500,8 @@ export class DocumentOptions {
     userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
 
     cardSort?: STRt = new StrInfo('way cards are sorted in deck view');
-    customSortNumber?: NUMt = new NumInfo('number of custom sorts the user has created');
-    // customGroup1?: List<Doc>
-    // customGroup2?: List<Doc>
-    // customGroup3?: List<Doc>
-    visibleGroupNumbers?: List<number>
-    custom1Group?: NUMt = new NumInfo('Which group a card is in for the 1st custom grouping');
-    custom2Group?: NUMt = new NumInfo('Which group a card is in for the 2nd custom grouping');
-    custom3Group?: NUMt = new NumInfo('Which group a card is in for the 3rd custom grouping');
-    chatGroup?: NUMt = new NumInfo("Which group a card is in for the chat's grouping");
-    chatAmGroups?: NUMt = new NumInfo("Number of cards created by chat sort");
-
-
-
-
-
-
-    // card_sort_time?: BOOLt = new BoolInfo('whether sorting cards in deck view by time');
-    // card_sort_type?: BOOLt = new BoolInfo('whether sorting cards in deck view by type');
-    // card_sort_color?: BOOLt = new BoolInfo('whether sorting cards in deck view by color');
-
-
+    cardSort_customField?: STRt = new StrInfo('field key used for sorting cards');
+    cardSort_visibleSortGroups?: List<number>; // which sorting values are being filtered (shown)
 }
 
 export const DocOptions = new DocumentOptions();
@@ -1239,8 +1220,6 @@ export namespace Docs {
             );
         }
 
-        
-
         export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id);
         }
@@ -1258,7 +1237,7 @@ export namespace Docs {
         }
 
         export function CardDeckDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Card});
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Card });
         }
 
         export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
@@ -1911,44 +1890,6 @@ export namespace DocUtils {
         }
     }
 
-    export function spreadCards(docList: Doc[], x: number = 0, y: number = 0, spreadAngle: number = 30, radius: number = 100, create: boolean = true) {
-        console.log('spread cards');
-        const totalCards = docList.length;
-        const halfSpreadAngle = spreadAngle * 0.5;
-        const angleStep = spreadAngle / (totalCards - 1);
-        
-        runInAction(() => {
-            docList.forEach((d, i) => {
-                DocUtils.iconify(d);
-                const angle = (-halfSpreadAngle + angleStep * i) * (Math.PI / 180); // Convert degrees to radians
-                d.x = x + Math.cos(angle) * radius;
-                d.y = y + Math.sin(angle) * radius;
-                d.rotation = angle; 
-                d._timecodeToShow = undefined;
-            });
-        });
-
-        if (create) {
-            const newCollection = Docs.Create.CardDeckDocument(docList, { 
-                title: 'card-spread', 
-                _freeform_noZoom: true, 
-                x: x - radius, 
-                y: y - radius, 
-                _width: radius * 2, 
-                _height: radius * 2, 
-                dragWhenActive: true, 
-                _layout_fitWidth: false 
-            });
-            // Adjust position based on the collection's dimensions if needed
-            newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - radius;
-            newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - radius;
-            newCollection._width = newCollection._height = radius * 2;
-            return newCollection;
-        }
-    }
-
-
-
     export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) {
         const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from');
         if (!portalLink) {
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index d507e35ad..36b926053 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -68,7 +68,6 @@ export class SelectionManager {
     public static IsSelected = (doc?: Doc) => Array.from(doc?.[DocViews] ?? []).some(dv => dv?.IsSelected);
     public static get Views() { return this.Instance.SelectedViews; } // prettier-ignore
     public static get SelectedSchemaDoc() { return this.Instance.SelectedSchemaDocument; } // prettier-ignore
-    
     public static get Docs() { return this.Instance.SelectedViews.map(dv => dv.Document).filter(doc => doc?._type_collection !== CollectionViewType.Docking); } // prettier-ignore
 }
 
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index ef4257937..de4df1830 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -35,7 +35,7 @@ export interface ViewBoxInterface {
     addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
     removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections)
     select?: (ctrlKey: boolean, shiftKey: boolean) => void;
-    focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>; 
+    focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>;
     viewTransition?: () => Opt<string>; // duration of a view transition animation
     isAnyChildContentActive?: () => boolean; // is any child content of the document active
     onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 9e5668ffa..5f8ddd5c1 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -75,7 +75,7 @@ export class CollectionCardView extends CollectionSubView() {
 
     componentDidMount(): void {
         this._disposers.sort = reaction(
-            () => ({ cardSort: this.cardSort, field: this.customSortField }),
+            () => ({ cardSort: this.cardSort, field: this.cardSort_customField }),
             ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false))
         );
     }
@@ -85,8 +85,8 @@ export class CollectionCardView extends CollectionSubView() {
         this._dropDisposer?.();
     }
 
-    @computed get customSortField() {
-        return StrCast(this.Document.customSortField) as any as 'chat' | 'star' | 'idea' | 'like';
+    @computed get cardSort_customField() {
+        return StrCast(this.Document.cardSort_customField) as any as 'chat' | 'star' | 'idea' | 'like';
     }
 
     @computed get cardSort() {
@@ -115,12 +115,12 @@ export class CollectionCardView extends CollectionSubView() {
      */
     @computed get childDocsWithoutLinks() {
         const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK);
-        const activeGroups = NumListCast(this.Document.visibleGroupNumbers);
+        const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups);
 
         if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) {
             return regularDocs.filter(doc => {
                 // Get the group number for the current index
-                const groupNumber = CollectionCardView.getButtonGroup(this.customSortField, doc);
+                const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
                 // Check if the group number is in the active groups
                 return groupNumber !== undefined && activeGroups.includes(groupNumber);
             });
@@ -245,8 +245,8 @@ export class CollectionCardView extends CollectionSubView() {
                                   return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string
                                           DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string
                     case cardSortings.Custom:
-                                  return [CollectionCardView.getButtonGroup(this.customSortField, docA)??0,
-                                          CollectionCardView.getButtonGroup(this.customSortField, docB)??0];
+                                  return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0,
+                                          CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0];
                     default:      return [StrCast(docA.type), // If docA.type is undefined, use an empty string
                                           StrCast(docB.type)]; // If docB.type is undefined, use an empty string
                     } // prettier-ignore
@@ -335,7 +335,7 @@ export class CollectionCardView extends CollectionSubView() {
      * @param buttonID
      * @param doc
      */
-    toggleButton = undoable((buttonID: number, doc: Doc) => this.customSortField && (doc[this.customSortField] = buttonID), 'toggle custom button');
+    toggleButton = undoable((buttonID: number, doc: Doc) => this.cardSort_customField && (doc[this.cardSort_customField] = buttonID), 'toggle custom button');
 
     /**
      * 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.
@@ -422,8 +422,8 @@ export class CollectionCardView extends CollectionSubView() {
      */
     renderButtons = (doc: Doc, cardSort: cardSortings) => {
         if (cardSort !== cardSortings.Custom) return '';
-        const amButtons = Math.max(4, this.childDocs?.reduce((set, doc) => this.customSortField && set.add(NumCast(doc[this.customSortField])), new Set<number>()).size ?? 0);
-        const activeButtonIndex = CollectionCardView.getButtonGroup(this.customSortField, doc);
+        const amButtons = Math.max(4, this.childDocs?.reduce((set, doc) => this.cardSort_customField && set.add(NumCast(doc[this.cardSort_customField])), new Set<number>()).size ?? 0);
+        const activeButtonIndex = CollectionCardView.getButtonGroup(this.cardSort_customField, doc);
         const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6;
         return (
             <div className="card-button-container" style={{ width: `${totalWidth}px` }}>
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 81d9f4eea..94896f277 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -321,8 +321,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
                 return this._freeform_commands;
             case CollectionViewType.Card:
                 return this._freeform_commands;
-            
-            
         }
     }
     private _commandRef = React.createRef<HTMLInputElement>();
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index b7805bf3f..97a0b7bef 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -137,8 +137,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} />;
-
-
         }
     };
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 22005eb23..c83c26509 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -125,32 +125,6 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
     return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
 }
 
-// export function computeCardDeckLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
-//     const docMap = new Map<string, PoolData>();
-//     const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
-//     const burstScale = NumCast(pivotDoc._starburstDocScale, 1);
-
-//     childPairs.forEach(({ layout, data }, i) => {
-//         const aspect = NumCast(layout._height) / NumCast(layout._width);
-//         const docSize = Math.min(Math.min(400, NumCast(layout._width)), Math.min(400, NumCast(layout._width)) / aspect) * burstScale;
-//         const deg = (i / childPairs.length) * Math.PI * 2;
-//         docMap.set(layout[Id], {
-//             x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)),
-//             y: Math.min(burstDiam[1] / 2 - docSize * aspect, Math.max(-burstDiam[1] / 2, (Math.sin(deg) * burstDiam[1]) / 2 - (docSize / 2) * aspect)),
-//             width: docSize,
-//             height: docSize * aspect,
-//             zIndex: NumCast(layout.zIndex),
-//             pair: { layout, data },
-//             replica: '',
-//             color: 'white',
-//             backgroundColor: 'white',
-//             transition: 'all 0.3s',
-//         });
-//     });
-//     const divider = { type: 'div', color: 'transparent', x: -burstDiam[0] / 2, y: -burstDiam[1] / 2, width: 15, height: 15, payload: undefined };
-//     return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
-// }
-
 export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const docMap = new Map<string, PoolData>();
     const fieldKey = 'data';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 11193f496..079a5d977 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -49,9 +49,7 @@ import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
 import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
-import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult,
-    //  computeCardDeckLayout 
-    } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
@@ -1385,8 +1383,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             case computeTimelineLayout.name:  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
             case computePivotLayout.name:     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
             case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
-            // case computeCardDeckLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeCardDeckLayout) };
-
         }
         return { newPool, computedElementData: this.doFreeformLayout(newPool) };
     }
@@ -2010,4 +2006,4 @@ ScriptingGlobals.add(function datavizFromSchema(doc: Doc) {
             SchemaCSVPopUp.Instance.setVisible(true);
         }
     });
-});
\ No newline at end of file
+});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index e44c37873..25a31fd6e 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -385,18 +385,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
         this.hideMarquee();
     });
 
-    // @undoBatch
-    // spreadCards = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
-    //     const selected = this.marqueeSelect(false);
-    //     SelectionManager.DeselectAll();
-    //     selected.forEach(d => this._props.removeDocument?.(d));
-    //     const newCollection = DocUtils.spreadCards(selected, this.Bounds.left + this.Bounds.width / 2, this.Bounds.top + this.Bounds.height / 2)!;
-    //     this._props.addDocument?.(newCollection);
-    //     this._props.selectDocuments([newCollection]);
-    //     MarqueeOptionsMenu.Instance.fadeOut(true);
-    //     this.hideMarquee();
-    // });
-
     /**
      * This triggers the TabDocView.PinDoc method which is the universal method
      * used to pin documents to the currently active presentation trail.
@@ -522,7 +510,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
 
     @action
     marqueeCommand = (e: KeyboardEvent) => {
-
         if (this._commandExecuted || (e as any).propagationIsStopped) {
             return;
         }
@@ -533,8 +520,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             this.delete(e, e.key === 'h');
             e.stopPropagation();
         }
-        if ('ctsSpgac'.indexOf(e.key) !== -1) {
-
+        if ('ctsSpg'.indexOf(e.key) !== -1) {
             this._commandExecuted = true;
             e.stopPropagation();
             e.preventDefault();
@@ -542,8 +528,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
             if (e.key === 'g') this.collection(e, true);
             if (e.key === 'c' || e.key === 't') this.collection(e);
             if (e.key === 's' || e.key === 'S') this.summary(e);
-            if (e.key === 'p') this.pileup(e)
-            // if (e.key === 'c') this.spreadCards(e);
+            if (e.key === 'p') this.pileup(e);
 
             this.cleanupInteractions(false);
         }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 731ea1235..6a956f2ac 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -51,7 +51,7 @@ export const FInfotoColType: { [key: string]: ColumnType } = {
     enumeration: ColumnType.Enumeration,
 };
 
-const  defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text'];
+const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text'];
 
 @observer
 export class CollectionSchemaView extends CollectionSubView() {
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 35a3a2e31..d5f6921a2 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -166,44 +166,44 @@ ScriptingGlobals.add(function showFreeform(
             setDoc: (doc: Doc, dv: DocumentView) => doc.cardSort = "links",
         }],
         ['like', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "like",
+            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "like",
             setDoc: (doc: Doc, dv: DocumentView) => {
                 doc.cardSort = "custom";
-                doc.customSortField = "like";
-                doc.visibleGroupNumbers = new List<number>();
+                doc.cardSort_customField = "like";
+                doc.cardSort_visibleSortGroups = new List<number>();
             }
         }],
         ['star', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "star",
+            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "star",
             setDoc: (doc: Doc, dv: DocumentView) => {
                 doc.cardSort = "custom";
-                doc.customSortField = "star";
-                doc.visibleGroupNumbers = new List<number>();
+                doc.cardSort_customField = "star";
+                doc.cardSort_visibleSortGroups = new List<number>();
             }
         }],
         ['idea', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.customSortField) === "idea",
+            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom" && StrCast(doc?.cardSort_customField) === "idea",
             setDoc: (doc: Doc, dv: DocumentView) => {
                 doc.cardSort = "custom";
-                doc.customSortField = "idea";
-                doc.visibleGroupNumbers = new List<number>();
+                doc.cardSort_customField = "idea";
+                doc.cardSort_visibleSortGroups = new List<number>();
             }
         }],
         ['chat', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom"  && StrCast(doc?.customSortField) === "chat",
+            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "custom"  && StrCast(doc?.cardSort_customField) === "chat",
             setDoc: (doc: Doc, dv: DocumentView) => {
                 doc.cardSort = "custom";
-                doc.customSortField = "chat";
-                doc.visibleGroupNumbers = new List<number>();
+                doc.cardSort_customField = "chat";
+                doc.cardSort_visibleSortGroups = new List<number>();
             },
         }],
     ]);
     for (let i = 0; i < 8; i++) {
         map.set((i + 1 + '') as any, {
-            checkResult: (doc: Doc) => NumListCast(doc?.visibleGroupNumbers).includes(i),
+            checkResult: (doc: Doc) => NumListCast(doc?.cardSort_visibleSortGroups).includes(i),
             setDoc: (doc: Doc, dv: DocumentView) => {
-                const list = NumListCast(doc.visibleGroupNumbers);
-                doc.visibleGroupNumbers = new List<number>(list.includes(i) ? list.filter(d => d !== i) : [...list, i]);
+                const list = NumListCast(doc.cardSort_visibleSortGroups);
+                doc.cardSort_visibleSortGroups = new List<number>(list.includes(i) ? list.filter(d => d !== i) : [...list, i]);
             },
         });
     }
@@ -219,7 +219,7 @@ ScriptingGlobals.add(function showFreeform(
 ScriptingGlobals.add(function cardHasLabel(label: string) {
     const selected = SelectionManager.Docs.lastElement();
     const labelNum = Number(label) - 1;
-    return labelNum < 4 || (selected && DocListCast(selected[Doc.LayoutFieldKey(selected)]).some(doc => doc[StrCast(selected.customSortField)] == labelNum));
+    return labelNum < 4 || (selected && DocListCast(selected[Doc.LayoutFieldKey(selected)]).some(doc => doc[StrCast(selected.cardSort_customField)] == labelNum));
 }, '');
 
 // ScriptingGlobals.add(function setCardSortAttr(attr: 'time' | 'docType' | 'color', value: any, checkResult?: boolean) {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 90d6133e4..3e0270aa3 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -29,11 +29,10 @@ import { CreateImage } from '../nodes/WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { DocumentView, OpenWhere } from './DocumentView';
-import { FocusViewOptions, FieldView, FieldViewProps } from './FieldView';
+import { FieldView, FieldViewProps, FocusViewOptions } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
 import { PinProps, PresBox } from './trails';
-import { Networking } from '../../Network';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index f856d9637..43010b2ed 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -972,8 +972,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
                 event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
                 icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
             });
-        optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
-        !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'sliders' });
+        !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
+        const help = cm.findByDescription('Help...');
+        const helpItems = help && 'subitems' in help ? help.subitems : [];
+        helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
+        !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' });
         this._downX = this._downY = Number.NaN;
     };
 
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 4fb757d8a..0ba62d60e 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -21,7 +21,7 @@ export enum GPTPopupMode {
     EDIT,
     IMAGE,
     DATA,
-    SORT
+    SORT,
 }
 
 interface GPTPopupProps {}
@@ -60,7 +60,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     public dataChatPrompt: string | null = null;
     @action
     public setDataJson = (text: string) => {
-        if (text=="") this.dataChatPrompt = "";
+        if (text == '') this.dataChatPrompt = '';
         this.dataJson = text;
     };
 
@@ -95,21 +95,18 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     private done: boolean = false;
     @action
     public setDone = (done: boolean) => {
-        console.log("HIIIIIIIII")
         this.done = done;
         this.chatMode = false;
     };
 
     @observable
-    private sortDone: boolean = false // this is so redundant but the og done variable was causing weird unknown problems and im just a girl
+    private sortDone: boolean = false; // this is so redundant but the og done variable was causing weird unknown problems and im just a girl
 
     @action
     public setSortDone = (done: boolean) => {
-        console.log("HIIIIIIIII")
         this.sortDone = done;
     };
 
-
     // change what can be a ref into a ref
     @observable
     private sidebarId: string = '';
@@ -133,26 +130,20 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     };
 
     @observable
-    public sortDesc: string = ''
+    public sortDesc: string = '';
 
-    @action public setSortDesc = (t:string) => {
-        this.sortDesc = t
-    }
+    @action public setSortDesc = (t: string) => {
+        this.sortDesc = t;
+    };
 
     @observable onSortComplete?: (sortResult: string) => void;
     @observable cardsDoneLoading = false;
 
     @action setCardsDoneLoading(done: boolean) {
-        console.log(done + "HI HIHI")
+        console.log(done + 'HI HIHI');
         this.cardsDoneLoading = done;
     }
 
-
-
-
-
-
-
     public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
     public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
 
@@ -164,12 +155,11 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
         this.setSortDone(false);
 
         try {
-
             const res = await gptAPICall(this.sortDesc, GPTCallType.SORT);
             // Trigger the callback with the result
             if (this.onSortComplete) {
                 this.onSortComplete(res || 'Something went wrong :(');
-                console.log(res)
+                console.log(res);
             }
         } catch (err) {
             console.error(err);
@@ -177,9 +167,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
 
         this.setLoading(false);
         this.setSortDone(true);
-    }
-
-
+    };
 
     /**
      * Generates a Dalle image and uploads it to the server.
@@ -219,7 +207,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
             console.error(err);
         }
         GPTPopup.Instance.setLoading(false);
-    }
+    };
 
     generateDataAnalysis = async () => {
         GPTPopup.Instance.setVisible(true);
@@ -231,7 +219,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
             console.error(err);
         }
         GPTPopup.Instance.setLoading(false);
-    }
+    };
 
     /**
      * Transfers the summarization text to a sidebar annotation text document.
@@ -282,7 +270,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
      */
     private chatWithAI = () => {
         this.chatMode = true;
-    }
+    };
     dataPromptChanged = action((e: React.ChangeEvent<HTMLInputElement>) => {
         this.dataChatPrompt = e.target.value;
     });
@@ -304,14 +292,14 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     sortBox = () => (
         <>
             <div>
-                {this.heading("SORTING")}
+                {this.heading('SORTING')}
                 {this.loading ? (
                     <div className="content-wrapper">
-                    <div className="loading-spinner">
-                        <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
-                        <span>Loading...</span>
+                        <div className="loading-spinner">
+                            <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userVariantColor)} height={30} width={30} />
+                            <span>Loading...</span>
+                        </div>
                     </div>
-                </div>
                 ) : (
                     <>
                         {!this.cardsDoneLoading ? (
@@ -334,27 +322,18 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
                                             width: '90%', // Almost as wide as the container
                                             textAlign: 'center',
                                             color: '#ffffff', // White text
-                                            fontSize: '16px' // Adjust font size as needed
+                                            fontSize: '16px', // Adjust font size as needed
                                         }}
                                     />
                                 </div>
                             )
                         )}
-    
+
                         {this.sortDone && (
                             <div>
                                 <div className="content-wrapper">
-                                    <p>
-                                        {this.text === "Something went wrong :(" ? 
-                                        "Something went wrong :(" : 
-                                        "Sorting done! Feel free to move things around / regenerate :) !"}
-                                    </p>
-                                    <IconButton
-                                        tooltip="Generate Again"
-                                        onClick={() => this.setSortDone(false)}
-                                        icon={<FontAwesomeIcon icon="redo-alt" size="lg" />}
-                                        color={StrCast(Doc.UserDoc().userVariantColor)}
-                                    />
+                                    <p>{this.text === 'Something went wrong :(' ? 'Something went wrong :(' : 'Sorting done! Feel free to move things around / regenerate :) !'}</p>
+                                    <IconButton tooltip="Generate Again" onClick={() => this.setSortDone(false)} icon={<FontAwesomeIcon icon="redo-alt" size="lg" />} color={StrCast(Doc.UserDoc().userVariantColor)} />
                                 </div>
                             </div>
                         )}
@@ -364,21 +343,17 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
         </>
     );
 
-    
-    
-
-
-//     <>
-//     <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
-//     <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
-// </>
-// ) : (
-// <div className="summarizing">
-//     <span>Sorting</span>
-//     <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
-//     <Button text="Stop Animation" onClick={() => {this.setDone(true);}} color={StrCast(Doc.UserDoc().userVariantColor)}  type={Type.TERT}/>
-// </div>
-// )}
+    //     <>
+    //     <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+    //     <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+    // </>
+    // ) : (
+    // <div className="summarizing">
+    //     <span>Sorting</span>
+    //     <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+    //     <Button text="Stop Animation" onClick={() => {this.setDone(true);}} color={StrCast(Doc.UserDoc().userVariantColor)}  type={Type.TERT}/>
+    // </div>
+    // )}
 
     imageBox = () => {
         return (
@@ -457,7 +432,7 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     dataAnalysisBox = () => (
         <>
             <div>
-                {this.heading("ANALYSIS")}
+                {this.heading('ANALYSIS')}
                 <div className="content-wrapper">
                     {!this.loading &&
                         (!this.done ? (
@@ -479,8 +454,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
             </div>
             {!this.loading && (
                 <div className="btns-wrapper">
-                    {this.done? 
-                        this.chatMode?(
+                    {this.done ? (
+                        this.chatMode ? (
                             <input
                                 defaultValue=""
                                 autoComplete="off"
@@ -493,19 +468,26 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
                                 placeholder="Ask GPT a question about the data..."
                                 id="search-input"
                                 className="searchBox-input"
-                                style={{width: "100%"}}
+                                style={{ width: '100%' }}
                             />
-                            )
-                        :(
-                        <>
-                            <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
-                            <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
-                        </>
+                        ) : (
+                            <>
+                                <Button tooltip="Transfer to text" text="Transfer To Text" onClick={this.transferToText} color={StrCast(Doc.UserDoc().userVariantColor)} type={Type.TERT} />
+                                <Button tooltip="Chat with AI" text="Chat with AI" onClick={this.chatWithAI} color={StrCast(Doc.UserDoc().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(Doc.UserDoc().userVariantColor)}  type={Type.TERT}/>
+                            <Button
+                                text="Stop Animation"
+                                onClick={() => {
+                                    this.setDone(true);
+                                }}
+                                color={StrCast(Doc.UserDoc().userVariantColor)}
+                                type={Type.TERT}
+                            />
                         </div>
                     )}
                 </div>
@@ -533,10 +515,8 @@ export class GPTPopup extends ObservableReactComponent<GPTPopupProps> {
     render() {
         return (
             <div className="summary-box" style={{ display: this.visible ? 'flex' : 'none' }}>
-                {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() :
-                this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() :
-                this.mode === GPTPopupMode.IMAGE ? this.imageBox() :
-                this.mode === GPTPopupMode.SORT ? this.sortBox() : <></>}            </div>
+                {this.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.mode === GPTPopupMode.DATA ? this.dataAnalysisBox() : this.mode === GPTPopupMode.IMAGE ? this.imageBox() : this.mode === GPTPopupMode.SORT ? this.sortBox() : <></>}{' '}
+            </div>
         );
     }
 }
-- 
cgit v1.2.3-70-g09d2


From d335e4694e5d45f6b273410664622ea1891073c8 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 20 May 2024 15:10:54 -0400
Subject: fixing flash card carousel code organization.  lint fixes.

---
 .../views/collections/CollectionCarouselView.tsx   | 130 ++++++++-------------
 src/client/views/collections/CollectionView.tsx    |  14 ---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  81 ++++++-------
 3 files changed, 84 insertions(+), 141 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index c3ba04aa8..282ac90fe 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -6,7 +6,7 @@ import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { emptyFunction } from '../../../Utils';
-import { StopEvent, returnFalse, returnOne, returnZero } from '../../../ClientUtils';
+import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
 import { Doc, Opt } from '../../../fields/Doc';
 import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -17,6 +17,8 @@ import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import './CollectionCarouselView.scss';
 import { CollectionSubView } from './CollectionSubView';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
 
 @observer
 export class CollectionCarouselView extends CollectionSubView() {
@@ -42,91 +44,49 @@ export class CollectionCarouselView extends CollectionSubView() {
         return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK);
     }
 
+    move = (dir: number) => {
+        const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => {
+            let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length;
+            while (!match(this.carouselItems?.[startInd].layout) && (startInd + dir + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
+                startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length;
+            }
+            this.layoutDoc._carousel_index = startInd;
+            return match(this.carouselItems?.[startInd].layout);
+        };
+        switch (StrCast(this.layoutDoc.filterOp)) {
+            case 'star':  // go to a flashcard that is starred, skip the ones that aren't
+                if (!moveToCardWithField((doc: Doc) => !!doc[`${this.fieldKey}_star`])) {
+                    this.layoutDoc.filterOp = undefined;  // if there aren't any starred, show all cards
+                }
+                break;
+            case 'practice': // go to a new index that is missed, skip the ones that are correct
+                if (!moveToCardWithField((doc: Doc) => doc[`${this.fieldKey}_missed`] !== 'correct')) {
+                    this.layoutDoc.filterOp = undefined;  // if all of the cards are correct, show all cards and exit practice mode
+
+                    // set all the cards to missed
+                    this.carouselItems.forEach(item => {
+                        item.layout[`${this.fieldKey}_missed`] = undefined;
+                    });
+                }
+                break;
+            default: moveToCardWithField( returnTrue);
+        } // prettier-ignore
+    };
+
     /**
-     * Goes to the next flashcard in the stack and filters
-     * based on the the currently selected option.
+     * Goes to the next Doc in the stack subject to the currently selected filter option.
      */
     advance = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.carouselItems.length;
-        let startInd = this.layoutDoc._carousel_index;
-
-        // if the star filter is selected
-        if (this.layoutDoc.filterOp === 'star') {
-            // go to a flashcard that is starred, skip the ones that aren't
-            while (!this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_star`] && (startInd + 1) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
-                startInd = (startInd + 1) % this.carouselItems.length;
-            }
-            this.layoutDoc._carousel_index = startInd;
-            // if there aren't any starred, show all cards
-            if (!this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_star`]) {
-                this.layoutDoc.filterOp = 'all';
-            }
-        }
-
-        // if the practice filter is selected
-        if (this.layoutDoc.filterOp === 'practice') {
-            // go to a new index that is missed, skip the ones that are correct
-            while (this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] === 'correct' && (startInd + 1) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
-                startInd = (startInd + 1) % this.carouselItems.length;
-            }
-            this.layoutDoc._carousel_index = startInd;
-
-            // if the user has gone through all of the cards and gotten them all correct, show all cards and exit practice mode
-            if (this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] === 'correct') {
-                this.layoutDoc.filterOp = 'all';
-
-                // set all the cards to missed
-                for (let i = 0; i < this.carouselItems.length; i++) {
-                    const curDoc = this.carouselItems?.[NumCast(i)];
-                    curDoc.layout[`${this.fieldKey}_missed`] = undefined;
-                }
-            }
-        }
+        this.move(1);
     };
 
     /**
-     * Goes to the previous flashcard in the stack and filters
-     * based on the the currently selected option.
+     * Goes to the previous Doc in the stack subject to the currently selected filter option.
      */
     goback = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.carouselItems.length) % this.carouselItems.length;
-
-        let startInd = this.layoutDoc._carousel_index;
-
-        // if the star filter is selected
-        if (this.layoutDoc.filterOp === 'star') {
-            // go to a new index that is starred, skip the ones that aren't
-            while (!this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_star`] && (startInd - 1 + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
-                startInd = (startInd - 1 + this.carouselItems.length) % this.carouselItems.length;
-            }
-            this.layoutDoc._carousel_index = startInd;
-            // if there aren't any starred, show all cards
-            if (!this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_star`]) {
-                this.layoutDoc.filterOp = 'all';
-            }
-        }
-
-        // if the practice filter is selected
-        if (this.layoutDoc.filterOp === 'practice') {
-            // go to a new index that is missed, skip the ones that are correct
-            while (this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] === 'correct' && (startInd - 1 + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) {
-                startInd = (startInd - 1 + this.carouselItems.length) % this.carouselItems.length;
-            }
-
-            this.layoutDoc._carousel_index = startInd;
-
-            // See all flashcards when finish going through practice mode and set all of the flashcards back to
-            if (this.carouselItems?.[NumCast(startInd)].layout[`${this.fieldKey}_missed`] === 'correct') {
-                this.layoutDoc.filterOp = 'all';
-
-                for (let i = 0; i < this.carouselItems.length; i++) {
-                    const curDoc = this.carouselItems?.[NumCast(i)];
-                    curDoc.layout[`${this.fieldKey}_missed`] = undefined;
-                }
-            }
-        }
+        this.move(-1);
     };
 
     /*
@@ -134,10 +94,8 @@ export class CollectionCarouselView extends CollectionSubView() {
      */
     star = (e: React.MouseEvent) => {
         e.stopPropagation();
-        const curDoc = this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)];
-        if (!curDoc) return;
-        if (curDoc.layout[`${this.fieldKey}_star`] === undefined) curDoc.layout[`${this.fieldKey}_star`] = true;
-        else curDoc.layout[`${this.fieldKey}_star`] = !curDoc.layout[`${this.fieldKey}_star`];
+        const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)];
+        curDoc.layout[`${this.fieldKey}_star`] = curDoc.layout[`${this.fieldKey}_star`] ? undefined : true;
     };
 
     /*
@@ -228,12 +186,24 @@ export class CollectionCarouselView extends CollectionSubView() {
             </>
         );
     }
+    specificMenu = (): void => {
+        const cm = ContextMenu.Instance;
+
+        const revealOptions = cm.findByDescription('Filter Flashcards');
+        const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
+        revealItems.push({description: 'All',           event: () => {this.layoutDoc.filterOp = undefined;},  icon: 'layer-group',}); // prettier-ignore
+        revealItems.push({description: 'Star',          event: () => {this.layoutDoc.filterOp = 'star';},     icon: 'star',}); // prettier-ignore
+        revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = 'practice';}, icon: 'check',}); // prettier-ignore
+        revealItems.push({description: 'Quiz Cards',    event: () => {this.layoutDoc.filterOp = 'quiz';},     icon: 'pencil',}); // prettier-ignore
+        !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
+    };
 
     render() {
         return (
             <div
                 className="collectionCarouselView-outer"
                 ref={this.createDashEventsTarget}
+                onContextMenu={this.specificMenu}
                 style={{
                     background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor),
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color),
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index d084a2aec..5c304b4a9 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -52,7 +52,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
         super(props);
         makeObservable(this);
         this._annotationKeySuffix = returnEmptyString;
-        this.layoutDoc[`filterOp`] = 'all';
     }
 
     componentDidMount() {
@@ -151,19 +150,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
                     return newRendition;
                 });
 
-            // creates menu option for flashcard filters
-            const revealOptions = cm.findByDescription('Filter Flashcards');
-            const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : [];
-            revealItems.push({ description: 'All', event: () => (this.layoutDoc[`filterOp`] = 'all'), icon: 'layer-group' });
-            revealItems.push({ description: 'Star', event: () => (this.layoutDoc[`filterOp`] = 'star'), icon: 'star' });
-            revealItems.push({ description: 'Practice Mode', event: () => (this.layoutDoc[`filterOp`] = 'practice'), icon: 'check' });
-            revealItems.push({ description: 'Quiz Cards', event: () => (this.layoutDoc[`filterOp`] = 'quiz'), icon: 'pencil' });
-
-            // only show the filter options if it is a collection of type Carousel view
-            if (this.Document?._type_collection === CollectionViewType.Carousel) {
-                !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' });
-            }
-
             const options = cm.findByDescription('Options...');
             const optionItems = options && 'subitems' in options ? options.subitems : [];
             !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.Document.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => {this.Document.forceActive = !this.Document.forceActive}, icon: 'project-diagram' }) : null; // prettier-ignore
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 110f7816c..be96b914c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -517,6 +517,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
                         }
                         break;
+                    default: 
                 }
             }
         }
@@ -767,10 +768,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     insideEraserOutline = (eraserOutline: InkData, point: { X: number; Y: number }) => {
         let isInside = false;
         if (!isNaN(eraserOutline[0].X) && !isNaN(eraserOutline[0].Y)) {
-            let minX = eraserOutline[0].X,
-                maxX = eraserOutline[0].X;
-            let minY = eraserOutline[0].Y,
-                maxY = eraserOutline[0].Y;
+            let [minX, minY] = [eraserOutline[0].X, eraserOutline[0].Y];
+            let [maxX, maxY] = [eraserOutline[0].X, eraserOutline[0].Y];
             for (let i = 1; i < eraserOutline.length; i++) {
                 const currPoint: { X: number; Y: number } = eraserOutline[i];
                 minX = Math.min(currPoint.X, minX);
@@ -784,7 +783,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
 
             for (let i = 0, j = eraserOutline.length - 1; i < eraserOutline.length; j = i, i++) {
-                if (eraserOutline[i].Y > point.Y != eraserOutline[j].Y > point.Y && point.X < ((eraserOutline[j].X - eraserOutline[i].X) * (point.Y - eraserOutline[i].Y)) / (eraserOutline[j].Y - eraserOutline[i].Y) + eraserOutline[i].X) {
+                if (eraserOutline[i].Y > point.Y !== eraserOutline[j].Y > point.Y && point.X < ((eraserOutline[j].X - eraserOutline[i].X) * (point.Y - eraserOutline[i].Y)) / (eraserOutline[j].Y - eraserOutline[i].Y) + eraserOutline[i].X) {
                     isInside = !isInside;
                 }
             }
@@ -845,7 +844,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
         // set distance of the eraser's bounding box based on the zoom
         let boundingBoxDist = ActiveEraserWidth() + 5;
-        this.zoomScaling() < 1 ? (boundingBoxDist = boundingBoxDist / (this.zoomScaling() * 1.5)) : (boundingBoxDist *= this.zoomScaling());
+        this.zoomScaling() < 1 ? (boundingBoxDist /= this.zoomScaling() * 1.5) : (boundingBoxDist *= this.zoomScaling());
 
         const eraserMin = { X: Math.min(lastPoint.X, currPoint.X) - boundingBoxDist, Y: Math.min(lastPoint.Y, currPoint.Y) - boundingBoxDist };
         const eraserMax = { X: Math.max(lastPoint.X, currPoint.X) + boundingBoxDist, Y: Math.max(lastPoint.Y, currPoint.Y) + boundingBoxDist };
@@ -961,29 +960,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) {
                         // the last end
                         break;
+                    } else if (!continueErasing) {
+                        currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT));
+                        continueErasing = true;
                     } else {
-                        if (!continueErasing) {
-                            currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT));
-                            continueErasing = true;
-                        } else {
-                            // we've reached the end of the part to take out...
-                            continueErasing = false;
-                            if (currSegment.length > 0) {
-                                segments.push(currSegment); // ...so we add it to the list and reset currSegment
-                                if (firstSegment.length === 0) {
-                                    firstSegment = currSegment;
-                                }
-                                currSegment = [];
+                        // we've reached the end of the part to take out...
+                        continueErasing = false;
+                        if (currSegment.length > 0) {
+                            segments.push(currSegment); // ...so we add it to the list and reset currSegment
+                            if (firstSegment.length === 0) {
+                                firstSegment = currSegment;
                             }
-                            currSegment.push(inkBezier.split(segmentTs[j] - currCurveT, 1));
+                            currSegment = [];
                         }
+                        currSegment.push(inkBezier.split(segmentTs[j] - currCurveT, 1));
                     }
                 }
-            } else {
-                if (!continueErasing) {
-                    // push the bezier piece if not in the eraser circle
-                    currSegment.push(inkBezier);
-                }
+            } else if (!continueErasing) {
+                // push the bezier piece if not in the eraser circle
+                currSegment.push(inkBezier);
             }
         }
 
@@ -1078,13 +1073,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
                         continueErasing = false;
                         hasSplit = true;
-                    } else {
-                        if (!continueErasing && !hasSplit) {
-                            // segment doesn't get pushed if continueErasing is true
-                            segment1.push(inkSegment);
-                        } else if (!continueErasing && hasSplit) {
-                            segment2.push(inkSegment);
-                        }
+                    } else if (!continueErasing && !hasSplit) {
+                        // segment doesn't get pushed if continueErasing is true
+                        segment1.push(inkSegment);
+                    } else if (!continueErasing && hasSplit) {
+                        segment2.push(inkSegment);
                     }
                 } else if (splitSegment1 === -1) {
                     // case where first end is erased
@@ -1096,13 +1089,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1));
                         }
                         hasSplit = true;
-                    } else {
-                        if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) {
-                            segment2.push(inkSegment.split(0, intersections.lastElement() - currCurveT));
-                            continueErasing = true;
-                        } else if (hasSplit && !continueErasing) {
-                            segment2.push(inkSegment);
-                        }
+                    } else if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) {
+                        segment2.push(inkSegment.split(0, intersections.lastElement() - currCurveT));
+                        continueErasing = true;
+                    } else if (hasSplit && !continueErasing) {
+                        segment2.push(inkSegment);
                     }
                 } else {
                     // case where last end is erased
@@ -1118,10 +1109,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT));
                         hasSplit = true;
                         continueErasing = true;
-                    } else {
-                        if ((isClosedCurve && hasSplit && !continueErasing) || (!isClosedCurve && !hasSplit)) {
-                            segment1.push(inkSegment);
-                        }
+                    } else if ((isClosedCurve && hasSplit && !continueErasing) || (!isClosedCurve && !hasSplit)) {
+                        segment1.push(inkSegment);
                     }
                 }
             }
@@ -1165,12 +1154,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 } else {
                     return this.getClosestTs(tVals, excludeT, mid + 1, endIndex);
                 }
+            } else if (mid - 1 >= startIndex && tVals[mid - 1] < excludeT) {
+                return [mid - 1, mid];
             } else {
-                if (mid - 1 >= startIndex && tVals[mid - 1] < excludeT) {
-                    return [mid - 1, mid];
-                } else {
-                    return this.getClosestTs(tVals, excludeT, startIndex, mid - 1);
-                }
+                return this.getClosestTs(tVals, excludeT, startIndex, mid - 1);
             }
         }
     };
-- 
cgit v1.2.3-70-g09d2

